├── .github └── ISSUE_TEMPLATE │ └── a-template-reminding-adal-s-status.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── RELEASES.md ├── adal.pyproj ├── adal.sln ├── adal ├── __init__.py ├── adal_error.py ├── argument.py ├── authentication_context.py ├── authentication_parameters.py ├── authority.py ├── cache_driver.py ├── code_request.py ├── constants.py ├── log.py ├── mex.py ├── oauth2_client.py ├── self_signed_jwt.py ├── token_cache.py ├── token_request.py ├── user_realm.py ├── util.py ├── wstrust_request.py ├── wstrust_response.py └── xmlutil.py ├── contributing.md ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── conf.py │ └── index.rst ├── pylintrc ├── requirements.txt ├── sample ├── certificate_credentials_sample.py ├── client_credentials_sample.py ├── device_code_sample.py ├── refresh_token_sample.py └── website_sample.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── config_sample.py ├── mex ├── address.insecure.xml ├── archan.us.mex.xml ├── arupela.mex.xml ├── common.mex.xml ├── microsoft.mex.xml ├── noaddress.xml ├── nobinding.port.xml ├── noname.binding.xml ├── nosoapaction.xml ├── nosoaptransport.xml ├── nouri.ref.xml ├── syntax.notrelated.mex.xml ├── syntax.related.mex.xml └── usystech.mex.xml ├── test_api_version.py ├── test_authentication_parameters.py ├── test_authority.py ├── test_authorization_code.py ├── test_cache_driver.py ├── test_client_credentials.py ├── test_e2e_examples.py ├── test_log.py ├── test_mex.py ├── test_refresh_token.py ├── test_self_signed_jwt.py ├── test_user_realm.py ├── test_username_password.py ├── test_wstrust_request.py ├── test_wstrust_response.py ├── util.py └── wstrust ├── RST.xml ├── RSTR.xml ├── common.base64.encoded.assertion.txt └── common.rstr.xml /.github/ISSUE_TEMPLATE/a-template-reminding-adal-s-status.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: A template reminding ADAL's status 3 | about: So that people are guided to use MSAL Python instead. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | This library, ADAL for Python, will no longer receive new feature improvements. Instead, use the new library [MSAL for Python](https://github.com/AzureAD/microsoft-authentication-library-for-python). 11 | 12 | * If you are starting a new project, you can get started with the MSAL Python docs for details about the scenarios, usage, and relevant concepts. 13 | * If your application is using the previous ADAL Python library, you can follow this migration guide to update to MSAL Python. 14 | * Existing applications relying on ADAL Python will continue to work. 15 | 16 | --- 17 | 18 | If you encounter a bug, please reproduce it using our off-the-shelf 19 | [samples](https://github.com/AzureAD/azure-activedirectory-library-for-python/tree/1.2.4/sample), so that we can follow your steps. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python cache 2 | __pycache__/ 3 | *.pyc 4 | 5 | # PTVS analysis 6 | .ptvs/ 7 | 8 | # Build results 9 | /bin/ 10 | /obj/ 11 | /dist/ 12 | /MANIFEST 13 | 14 | # Result of running python setup.py install/pip install -e 15 | /build/ 16 | /adal.egg-info/ 17 | 18 | # Test results 19 | /TestResults/ 20 | 21 | # User-specific files 22 | *.suo 23 | *.user 24 | *.sln.docstates 25 | /tests/config.py 26 | 27 | # Windows image file caches 28 | Thumbs.db 29 | ehthumbs.db 30 | 31 | # Folder config file 32 | Desktop.ini 33 | 34 | # Recycle Bin used on file shares 35 | $RECYCLE.BIN/ 36 | 37 | # Mac desktop service store files 38 | .DS_Store 39 | 40 | .idea 41 | src/build 42 | *.iml 43 | /doc/_build 44 | 45 | # Virtual Environments 46 | /env* 47 | 48 | # Visual Studio Files 49 | /.vs/* 50 | /tests/.vs/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.5" 6 | - "3.6" 7 | install: 8 | - pip install -r requirements.txt 9 | script: 10 | - # PyLint does not yet support Python 3.6 https://github.com/PyCQA/pylint/issues/1241 11 | if [ "$TRAVIS_PYTHON_VERSION" != "3.6" ]; then 12 | pylint adal; 13 | fi 14 | - python -m unittest discover -s tests 15 | 16 | deploy: 17 | - # test pypi 18 | provider: pypi 19 | distributions: "sdist bdist_wheel" 20 | server: https://test.pypi.org/legacy/ 21 | user: "nugetaad" 22 | password: 23 | secure: Wm2jGolFLm/wrfSPklf9gYdWiTK7ycGr+Qa0voVmFEJkW69PRC5bCibJI3POK1DqTBmQn7gi5G0s117PoLlXvK9lqwMaDL6Yf/ro7YnMU9pBopoB/zWMxWYZeBJVugmTGKuTkbUiQBzL2h0EnaQvvyrEDiLGrYrYEgLUSuR5AVTlvYKk1XBAAhvh8hu8JjgQQugN2ne6ZR9aBjCap0fzdTs3vhad/OQx+iH8YR8UTl4ruszdoL95CDtFmKdIkwg0qgIB65MqC6XAQ2tvhyMDHXZMMafE0NQwUwm2d+sqinCfHLNkb5bVBS0M8syrYCS8xr6Ccnt0PM1+nNFm83bu1w+HaMwKWD2IaU26QH8H7djc7mO1XmRmMSxQ1EjR313YyF534+uiLBlJWB8DOfN4r3/lqg6e44CY0impiT7NnT47bUqaoglew5HB0FgrrtGDrDlLa7zf+RHyb2BhqeqlTR1s0nnzsmzQMdxaHXvCbzYPqg3PUdwLHGBks90tXhA0zUg/3XQfb7v17Lx1byRufvsWWYXUZwLI6H8CCvWtWFvJ3TSPPBR/5LjaICVtt2g3Uv2xrG3weCIO52G7WQ6pIpOyiRsYkUAIXLi2UNsv4LlpNxNObNgL7FNfrNR/tEs8+SdbAkaf2jrFfn+Sk7v4pdPd4og7YXWAE2R/ge9nsJ4= 24 | on: 25 | branch: master 26 | tags: false 27 | # At Apr 2021, the get-pip.py used by Travis CI requires Python 3.6+ 28 | condition: $TRAVIS_PYTHON_VERSION = "3.6" 29 | 30 | - # production pypi 31 | provider: pypi 32 | distributions: "sdist bdist_wheel" 33 | user: "nugetaad" 34 | password: 35 | secure: Wm2jGolFLm/wrfSPklf9gYdWiTK7ycGr+Qa0voVmFEJkW69PRC5bCibJI3POK1DqTBmQn7gi5G0s117PoLlXvK9lqwMaDL6Yf/ro7YnMU9pBopoB/zWMxWYZeBJVugmTGKuTkbUiQBzL2h0EnaQvvyrEDiLGrYrYEgLUSuR5AVTlvYKk1XBAAhvh8hu8JjgQQugN2ne6ZR9aBjCap0fzdTs3vhad/OQx+iH8YR8UTl4ruszdoL95CDtFmKdIkwg0qgIB65MqC6XAQ2tvhyMDHXZMMafE0NQwUwm2d+sqinCfHLNkb5bVBS0M8syrYCS8xr6Ccnt0PM1+nNFm83bu1w+HaMwKWD2IaU26QH8H7djc7mO1XmRmMSxQ1EjR313YyF534+uiLBlJWB8DOfN4r3/lqg6e44CY0impiT7NnT47bUqaoglew5HB0FgrrtGDrDlLa7zf+RHyb2BhqeqlTR1s0nnzsmzQMdxaHXvCbzYPqg3PUdwLHGBks90tXhA0zUg/3XQfb7v17Lx1byRufvsWWYXUZwLI6H8CCvWtWFvJ3TSPPBR/5LjaICVtt2g3Uv2xrG3weCIO52G7WQ6pIpOyiRsYkUAIXLi2UNsv4LlpNxNObNgL7FNfrNR/tEs8+SdbAkaf2jrFfn+Sk7v4pdPd4og7YXWAE2R/ge9nsJ4= 36 | on: 37 | branch: master 38 | tags: true 39 | # At Apr 2021, the get-pip.py used by Travis CI requires Python 3.6+ 40 | condition: $TRAVIS_PYTHON_VERSION = "3.6" 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Microsoft Corporation. 4 | All rights reserved. 5 | 6 | This code is licensed under the MIT License. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files(the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions : 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | This library, ADAL for Python, will no longer receive new feature improvements. Instead, use the new library 4 | [MSAL for Python](https://github.com/AzureAD/microsoft-authentication-library-for-python). 5 | 6 | * If you are starting a new project, you can get started with the 7 | [MSAL Python docs](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki) 8 | for details about the scenarios, usage, and relevant concepts. 9 | * If your application is using the previous ADAL Python library, you can follow this 10 | [migration guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/migrate-python-adal-msal) 11 | to update to MSAL Python. 12 | * Existing applications relying on ADAL Python will continue to work. 13 | 14 | --- 15 | 16 | 17 | # Microsoft Azure Active Directory Authentication Library (ADAL) for Python 18 | 19 | `master` branch | `dev` branch | Reference Docs 20 | --------------------|-----------------|--------------- 21 | [![Build Status](https://travis-ci.org/AzureAD/azure-activedirectory-library-for-python.svg?branch=master)](https://travis-ci.org/AzureAD/azure-activedirectory-library-for-python) | [![Build Status](https://travis-ci.org/AzureAD/azure-activedirectory-library-for-python.svg?branch=dev)](https://travis-ci.org/AzureAD/azure-activedirectory-library-for-python) | [![Documentation Status](https://readthedocs.org/projects/adal-python/badge/?version=latest)](https://adal-python.readthedocs.io/en/latest/?badge=latest) 22 | 23 | |[Getting Started](https://github.com/AzureAD/azure-activedirectory-library-for-python/wiki)| [Docs](https://aka.ms/aaddev)| [Python Samples](https://github.com/Azure-Samples?q=active-directory&language=python)| [Support](README.md#community-help-and-support) | [Feedback](https://forms.office.com/r/wX0UuEF8kX) 24 | | --- | --- | --- | --- | --- | 25 | 26 | 27 | The ADAL for Python library enables python applications to authenticate with Azure AD and get tokens to access Azure AD protected web resources. 28 | 29 | You can learn in detail about ADAL Python functionality and usage documented in the [Wiki](https://github.com/AzureAD/azure-activedirectory-library-for-python/wiki). 30 | 31 | ## Installation and Usage 32 | 33 | You can find the steps to install and basic usage of the library under [ADAL Basics](https://github.com/AzureAD/azure-activedirectory-library-for-python/wiki/ADAL-basics) page in the Wiki. 34 | 35 | ## Samples and Documentation 36 | We provide a full suite of [Python sample applications on GitHub](https://github.com/Azure-Samples?q=active-directory&language=python) to help you get started with learning the Azure Identity system. This will include tutorials for native clients and web applications. We also provide full walkthroughs for authentication flows such as OAuth2, OpenID Connect and for calling APIs such as the Graph API. 37 | 38 | There are also some [lightweight samples existing inside this repo](https://github.com/AzureAD/azure-activedirectory-library-for-python/tree/dev/sample). 39 | 40 | You can find the relevant samples by scenarios listed in this [wiki page for acquiring tokens using ADAL Python](https://github.com/AzureAD/azure-activedirectory-library-for-python/wiki/Acquire-tokens#adal-python-apis-for-corresponding-flows). 41 | 42 | The documents on [Auth Scenarios](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-authentication-scenarios#application-types-and-scenarios) and [Auth protocols](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-openid-connect-code) are recommended reading. 43 | 44 | ## Versions 45 | 46 | This library follows [Semantic Versioning](https://semver.org/). 47 | 48 | You can find the changes for each version under [Releases](https://github.com/AzureAD/azure-activedirectory-library-for-python/releases). 49 | 50 | ## Community Help and Support 51 | 52 | We leverage [Stack Overflow](https://stackoverflow.com/) to work with the community on supporting Azure Active Directory and its SDKs, including this one! We highly recommend you ask your questions on Stack Overflow (we're all on there!) Also browser existing issues to see if someone has had your question before. 53 | 54 | We recommend you use the "adal" tag so we can see it! Here is the latest Q&A on Stack Overflow for ADAL: [https://stackoverflow.com/questions/tagged/adal](https://stackoverflow.com/questions/tagged/adal) 55 | 56 | ## Submit Feedback 57 | We'd like your thoughts on this library. Please complete [this short survey.](https://forms.office.com/r/wX0UuEF8kX) 58 | 59 | ## Security Reporting 60 | 61 | If you find a security issue with our libraries or services please report it to [secure@microsoft.com](mailto:secure@microsoft.com) with as much detail as possible. Your submission may be eligible for a bounty through the [Microsoft Bounty](https://aka.ms/bugbounty) program. Please do not post security issues to GitHub Issues or any other public site. We will contact you shortly upon receiving the information. We encourage you to get notifications of when security incidents occur by visiting [this page](https://technet.microsoft.com/en-us/security/dd252948) and subscribing to Security Advisory Alerts. 62 | 63 | ## Contributing 64 | 65 | All code is licensed under the MIT license and we triage actively on GitHub. We enthusiastically welcome contributions and feedback. Please read the [contributing guide](./contributing.md) before starting. 66 | 67 | ## We Value and Adhere to the Microsoft Open Source Code of Conduct 68 | 69 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 70 | -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | # Microsoft Identity SDK Versioning and Servicing FAQ 2 | 3 | We have adopted the semantic versioning flow that is industry standard for OSS projects. It gives the maximum amount of control on what risk you take with what versions. If you know how semantic versioning works with node.js, java, and ruby none of this will be new. 4 | 5 | ##Semantic Versioning and API stability promises 6 | 7 | Microsoft Identity libraries are independent open source libraries that are used by partners both internal and external to Microsoft. As with the rest of Microsoft, we have moved to a rapid iteration model where bugs are fixed daily and new versions are produced as required. To communicate these frequent changes to external partners and customers, we use semantic versioning for all our public Microsoft Identity SDK libraries. This follows the practices of other open source libraries on the internet. This allows us to support our downstream partners which will lock on certain versions for stability purposes, as well as providing for the distribution over NuGet, CocoaPods, and Maven. 8 | 9 | The semantics are: MAJOR.MINOR.PATCH (example 1.1.5) 10 | 11 | We will update our code distributions to use the latest PATCH semantic version number in order to make sure our customers and partners get the latest bug fixes. Downstream partner needs to pull the latest PATCH version. Most partners should try lock on the latest MINOR version number in their builds and accept any updates in the PATCH number. 12 | 13 | Examples: 14 | Using Cocapods, the following in the podfile will take the latest ADALiOS build that is > 1.1 but not 1.2. 15 | ``` 16 | pod 'ADALiOS', '~> 1.1' 17 | ``` 18 | 19 | Using NuGet, this ensures all 1.1.0 to 1.1.x updates are included when building your code, but not 1.2. 20 | 21 | ``` 22 | 26 | ``` 27 | 28 | | Version | Description | Example | 29 | |:-------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------:| 30 | | x.x.x | PATCH version number. Incrementing these numbers is for bug fixes and updates but do not introduce new features. This is used for close partners who build on our platform release (ex. Azure AD Fabric, Office, etc.),In addition, Cocoapods, NuGet, and Maven use this number to deliver the latest release to customers.,This will update frequently (sometimes within the same day),There is no new features, and no regressions or API surface changes. Code will continue to work unless affected by a particular code fix. | ADAL for iOS 1.0.10,(this was a fix for the Storyboard display that was fixed for a specific Office team) | 31 | | x.x | MINOR version numbers. Incrementing these second numbers are for new feature additions that do not impact existing features or introduce regressions. They are purely additive, but may require testing to ensure nothing is impacted.,All x.x.x bug fixes will also roll up in to this number.,There is no regressions or API surface changes. Code will continue to work unless affected by a particular code fix or needs this new feature. | ADAL for iOS 1.1.0,(this added WPJ capability to ADAL, and rolled all the updates from 1.0.0 to 1.0.12) | 32 | | x | MAJOR version numbers. This should be considered a new, supported version of Microsoft Identity SDK and begins the Azure two year support cycle anew. Major new features are introduced and API changes can occur.,This should only be used after a large amount of testing and used only if those features are needed.,We will continue to service MAJOR version numbers with bug fixes up to the two year support cycle. | ADAL for iOS 1.0,(our first official release of ADAL) | 33 | 34 | 35 | 36 | ## Serviceability 37 | 38 | When we release a new MINOR version, the previous MINOR version is abandoned. 39 | 40 | When we release a new MAJOR version, we will continue to apply bug fixes to the existing features in the previous MAJOR version for up to the 2 year support cycle for Azure. 41 | Example: We release ADALiOS 2.0 in the future which supports unified Auth for AAD and MSA. Later, we then have a fix in Conditional Access for ADALiOS. Since that feature exists both in ADALiOS 1.1 and ADALiOS 2.0, we will fix both. It will roll up in a PATCH number for each. Customers that are still locked down on ADALiOS 1.1 will receive the benefit of this fix. 42 | 43 | ## Microsoft Identity SDKs and Azure Active Directory 44 | 45 | Microsoft Identity SDKs major versions will maintain backwards compatibility with Azure Active Directory web services through the support period. This means that the API surface area defined in a MAJOR version will continue to work for 2 years after release. 46 | 47 | We will respond to bugs quickly from our partners and customers submitted through GitHub and through our private alias (tellaad@microsoft.com) for security issues and update the PATCH version number. We will also submit a change summary for each PATCH number. 48 | Occasionally, there will be security bugs or breaking bugs from our partners that will require an immediate fix and a publish of an update to all partners and customers. When this occurs, we will do an emergency roll up to a PATCH version number and update all our distribution methods to the latest. 49 | -------------------------------------------------------------------------------- /adal.pyproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | 2.0 6 | a3fd1cd9-55d1-4dc7-a7e3-124dcfd9c010 7 | 8 | 9 | tests\__init__.py 10 | 11 | 12 | . 13 | . 14 | adal 15 | adal 16 | {4e6b690d-870d-4e48-b24e-ba6f9fe36da5} 17 | 3.5 18 | 19 | 20 | true 21 | false 22 | 23 | 24 | true 25 | false 26 | 27 | 28 | 29 | Code 30 | 31 | 32 | Code 33 | 34 | 35 | Code 36 | 37 | 38 | Code 39 | 40 | 41 | Code 42 | 43 | 44 | 45 | Code 46 | 47 | 48 | Code 49 | 50 | 51 | Code 52 | 53 | 54 | Code 55 | 56 | 57 | Code 58 | 59 | 60 | Code 61 | 62 | 63 | Code 64 | 65 | 66 | Code 67 | 68 | 69 | Code 70 | 71 | 72 | Code 73 | 74 | 75 | Code 76 | 77 | 78 | Code 79 | 80 | 81 | Code 82 | 83 | 84 | Code 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | Code 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | {035af01c-b03d-490d-8832-e92e10c726f3} 149 | {2af0f10d-7135-4994-9156-5d01c9c11b7e} 150 | 2.7 151 | env27 (Python 2.7) 152 | Scripts\python.exe 153 | Scripts\pythonw.exe 154 | Lib\ 155 | PYTHONPATH 156 | X86 157 | 158 | 159 | {ffb82c48-90d8-4438-b920-50f90148b4d6} 160 | {2af0f10d-7135-4994-9156-5d01c9c11b7e} 161 | 3.4 162 | env34 (Python 3.4) 163 | Scripts\python.exe 164 | Scripts\pythonw.exe 165 | Lib\ 166 | PYTHONPATH 167 | X86 168 | 169 | 170 | {4e6b690d-870d-4e48-b24e-ba6f9fe36da5} 171 | {2af0f10d-7135-4994-9156-5d01c9c11b7e} 172 | 3.5 173 | env35 (Python 3.5) 174 | Scripts\python.exe 175 | Scripts\pythonw.exe 176 | Lib\ 177 | PYTHONPATH 178 | X86 179 | 180 | 181 | 182 | 10.0 183 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets 184 | 185 | 186 | 187 | 190 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /adal.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "ADAL", "adal.pyproj", "{A3FD1CD9-55D1-4DC7-A7E3-124DCFD9C010}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A3FD1CD9-55D1-4DC7-A7E3-124DCFD9C010}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A3FD1CD9-55D1-4DC7-A7E3-124DCFD9C010}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | EndGlobal 21 | -------------------------------------------------------------------------------- /adal/__init__.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | # pylint: disable=wrong-import-position 29 | 30 | __version__ = '1.2.7' 31 | 32 | import logging 33 | 34 | from .authentication_context import AuthenticationContext 35 | from .token_cache import TokenCache 36 | from .log import (set_logging_options, 37 | get_logging_options, 38 | ADAL_LOGGER_NAME) 39 | from .adal_error import AdalError 40 | 41 | # to avoid "No handler found" warnings. 42 | logging.getLogger(ADAL_LOGGER_NAME).addHandler(logging.NullHandler()) 43 | 44 | 45 | -------------------------------------------------------------------------------- /adal/adal_error.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | class AdalError(Exception): 29 | def __init__(self, error_msg, error_response=None): 30 | super(AdalError, self).__init__(error_msg) 31 | self.error_response = error_response 32 | -------------------------------------------------------------------------------- /adal/argument.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | from .constants import OAuth2DeviceCodeResponseParameters 28 | 29 | def validate_user_code_info(user_code_info): 30 | if not user_code_info: 31 | raise ValueError("the user_code_info parameter is required") 32 | 33 | if not user_code_info.get(OAuth2DeviceCodeResponseParameters.DEVICE_CODE): 34 | raise ValueError("the user_code_info is missing device_code") 35 | 36 | if not user_code_info.get(OAuth2DeviceCodeResponseParameters.INTERVAL): 37 | raise ValueError("the user_code_info is missing internal") 38 | 39 | if not user_code_info.get(OAuth2DeviceCodeResponseParameters.EXPIRES_IN): 40 | raise ValueError("the user_code_info is missing expires_in") 41 | -------------------------------------------------------------------------------- /adal/authentication_parameters.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | #Note, this module does not appear being used anywhere 29 | 30 | import re 31 | 32 | import requests 33 | 34 | from . import util 35 | from . import log 36 | 37 | from .constants import HttpError 38 | 39 | AUTHORIZATION_URI = 'authorization_uri' 40 | RESOURCE = 'resource' 41 | WWW_AUTHENTICATE_HEADER = 'www-authenticate' 42 | 43 | # pylint: disable=anomalous-backslash-in-string,too-few-public-methods 44 | 45 | class AuthenticationParameters(object): 46 | 47 | def __init__(self, authorization_uri, resource): 48 | 49 | self.authorization_uri = authorization_uri 50 | self.resource = resource 51 | 52 | 53 | # The 401 challenge is a standard defined in RFC6750, which is based in part on RFC2617. 54 | # The challenge has the following form. 55 | # WWW-Authenticate : Bearer 56 | # authorization_uri="https://login.microsoftonline.com/mytenant.com/oauth2/authorize", 57 | # Resource_id="00000002-0000-0000-c000-000000000000" 58 | 59 | # This regex is used to validate the structure of the challenge header. 60 | # Match whole structure: ^\s*Bearer\s+([^,\s="]+?)="([^"]*?)"\s*(,\s*([^,\s="]+?)="([^"]*?)"\s*)*$ 61 | # ^ Start at the beginning of the string. 62 | # \s*Bearer\s+ Match 'Bearer' surrounded by one or more amount of whitespace. 63 | # ([^,\s="]+?) This captures the key which is composed of any characters except 64 | # comma, whitespace or a quotes. 65 | # = Match the = sign. 66 | # "([^"]*?)" Captures the value can be any number of non quote characters. 67 | # At this point only the first key value pair as been captured. 68 | # \s* There can be any amount of white space after the first key value pair. 69 | # ( Start a capture group to retrieve the rest of the key value 70 | # pairs that are separated by commas. 71 | # \s* There can be any amount of whitespace before the comma. 72 | # , There must be a comma. 73 | # \s* There can be any amount of whitespace after the comma. 74 | # (([^,\s="]+?) This will capture the key that comes after the comma. It's made 75 | # of a series of any character except comma, whitespace or quotes. 76 | # = Match the equal sign between the key and value. 77 | # " Match the opening quote of the value. 78 | # ([^"]*?) This will capture the value which can be any number of non 79 | # quote characters. 80 | # " Match the values closing quote. 81 | # \s* There can be any amount of whitespace before the next comma. 82 | # )* Close the capture group for key value pairs. There can be any 83 | # number of these. 84 | # $ The rest of the string can be whitespace but nothing else up to 85 | # the end of the string. 86 | # 87 | 88 | # This regex checks the structure of the whole challenge header. The complete 89 | # header needs to be checked for validity before we can be certain that 90 | # we will succeed in pulling out the individual parts. 91 | bearer_challenge_structure_validation = re.compile( 92 | """^\s*Bearer\s+([^,\s="]+?)="([^"]*?)"\s*(,\s*([^,\s="]+?)="([^"]*?)"\s*)*$""") 93 | # This regex pulls out the key and value from the very first pair. 94 | first_key_value_pair_regex = re.compile("""^\s*Bearer\s+([^,\s="]+?)="([^"]*?)"\s*""") 95 | 96 | # This regex is used to pull out all of the key value pairs after the first one. 97 | # All of these begin with a comma. 98 | all_other_key_value_pair_regex = re.compile("""(?:,\s*([^,\s="]+?)="([^"]*?)"\s*)""") 99 | 100 | 101 | def parse_challenge(challenge): 102 | 103 | if not bearer_challenge_structure_validation.search(challenge): 104 | raise ValueError("The challenge is not parseable as an RFC6750 OAuth2 challenge") 105 | 106 | challenge_parameters = {} 107 | match = first_key_value_pair_regex.search(challenge) 108 | if match: 109 | challenge_parameters[match.group(1)] = match.group(2) 110 | 111 | for match in all_other_key_value_pair_regex.finditer(challenge): 112 | challenge_parameters[match.group(1)] = match.group(2) 113 | 114 | return challenge_parameters 115 | 116 | def create_authentication_parameters_from_header(challenge): 117 | challenge_parameters = parse_challenge(challenge) 118 | authorization_uri = challenge_parameters.get(AUTHORIZATION_URI) 119 | 120 | if not authorization_uri: 121 | raise ValueError("Could not find 'authorization_uri' in challenge header.") 122 | 123 | resource = challenge_parameters.get(RESOURCE) 124 | return AuthenticationParameters(authorization_uri, resource) 125 | 126 | def create_authentication_parameters_from_response(response): 127 | 128 | if response is None: 129 | raise AttributeError('Missing required parameter: response') 130 | 131 | if not hasattr(response, 'status_code') or not response.status_code: 132 | raise AttributeError('The response parameter does not have the expected HTTP status_code field') 133 | 134 | if not hasattr(response, 'headers') or not response.headers: 135 | raise AttributeError('There were no headers found in the response.') 136 | 137 | if response.status_code != HttpError.UNAUTHORIZED: 138 | raise ValueError('The response status code does not correspond to an OAuth challenge. ' 139 | 'The statusCode is expected to be 401 but is: {}'.format(response.status_code)) 140 | 141 | challenge = response.headers.get(WWW_AUTHENTICATE_HEADER) 142 | if not challenge: 143 | raise ValueError("The response does not contain a WWW-Authenticate header that can be " 144 | "used to determine the authority_uri and resource.") 145 | 146 | return create_authentication_parameters_from_header(challenge) 147 | 148 | def validate_url_object(url): 149 | if not url or not hasattr(url, 'geturl'): 150 | raise AttributeError('Parameter is of wrong type: url') 151 | 152 | def create_authentication_parameters_from_url(url, correlation_id=None): 153 | 154 | if isinstance(url, str): 155 | challenge_url = url 156 | else: 157 | validate_url_object(url) 158 | challenge_url = url.geturl() 159 | 160 | log_context = log.create_log_context(correlation_id) 161 | logger = log.Logger('AuthenticationParameters', log_context) 162 | 163 | logger.debug( 164 | "Attempting to retrieve authentication parameters from: {}".format(challenge_url) 165 | ) 166 | 167 | class _options(object): 168 | _call_context = {'log_context': log_context} 169 | 170 | options = util.create_request_options(_options()) 171 | try: 172 | response = requests.get(challenge_url, headers=options['headers']) 173 | except Exception: 174 | logger.info("Authentication parameters http get failed.") 175 | raise 176 | 177 | try: 178 | return create_authentication_parameters_from_response(response) 179 | except Exception: 180 | logger.info("Unable to parse response in to authentication parameters.") 181 | raise 182 | -------------------------------------------------------------------------------- /adal/authority.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | try: 29 | from urllib.parse import quote, urlparse 30 | except ImportError: 31 | from urllib import quote # pylint: disable=no-name-in-module 32 | from urlparse import urlparse # pylint: disable=import-error,ungrouped-imports 33 | 34 | import requests 35 | 36 | from .constants import AADConstants 37 | from .adal_error import AdalError 38 | from . import log 39 | from . import util 40 | 41 | class Authority(object): 42 | 43 | def __init__(self, authority_url, validate_authority=True): 44 | 45 | self._log = None 46 | self._call_context = None 47 | self._url = urlparse(authority_url) 48 | 49 | self._validate_authority_url() 50 | self._validated = not validate_authority 51 | 52 | self._host = None 53 | self._tenant = None 54 | self._parse_authority() 55 | 56 | self._authorization_endpoint = None 57 | self.token_endpoint = None 58 | self.device_code_endpoint = None 59 | self.is_adfs_authority = self._tenant.lower() == 'adfs' 60 | 61 | @property 62 | def url(self): 63 | return self._url.geturl() 64 | 65 | def _whitelisted(self): # testing if self._url.hostname is a dsts whitelisted domain 66 | # Add dSTS domains to whitelist based on based on domain 67 | # https://microsoft.sharepoint.com/teams/AzureSecurityCompliance/Security/SitePages/dSTS%20Fundamentals.aspx 68 | return ".dsts." in self._url.hostname 69 | 70 | def _validate_authority_url(self): 71 | 72 | if self._url.scheme != 'https': 73 | raise ValueError("The authority url must be an https endpoint.") 74 | 75 | if self._url.query: 76 | raise ValueError("The authority url must not have a query string.") 77 | 78 | path_parts = [part for part in self._url.path.split('/') if part] 79 | if (len(path_parts) > 1) and (not self._whitelisted()): #if dsts host, path_parts will be 2 80 | raise ValueError( 81 | "The path of authority_url (also known as tenant) is invalid, " 82 | "it should either be a domain name (e.g. mycompany.onmicrosoft.com) " 83 | "or a tenant GUID id. " 84 | 'Your tenant input was "%s" and your entire authority_url was "%s".' 85 | % ('/'.join(path_parts), self._url.geturl())) 86 | elif len(path_parts) == 1: 87 | self._url = urlparse(self._url.geturl().rstrip('/')) 88 | 89 | def _parse_authority(self): 90 | self._host = self._url.hostname 91 | 92 | path_parts = self._url.path.split('/') 93 | try: 94 | self._tenant = path_parts[1] 95 | except IndexError: 96 | raise ValueError("Could not determine tenant.") 97 | 98 | def _perform_static_instance_discovery(self): 99 | 100 | self._log.debug("Performing static instance discovery") 101 | 102 | if self._whitelisted(): # testing if self._url.hostname is a dsts whitelisted domain 103 | self._log.debug("Authority validated via static instance discovery") 104 | return True 105 | try: 106 | AADConstants.WELL_KNOWN_AUTHORITY_HOSTS.index(self._url.hostname) 107 | except ValueError: 108 | return False 109 | 110 | self._log.debug("Authority validated via static instance discovery") 111 | return True 112 | 113 | def _create_authority_url(self): 114 | return "https://{}/{}{}".format(self._url.hostname, 115 | self._tenant, 116 | AADConstants.AUTHORIZE_ENDPOINT_PATH) 117 | 118 | def _create_instance_discovery_endpoint_from_template(self, authority_host): 119 | 120 | discovery_endpoint = AADConstants.INSTANCE_DISCOVERY_ENDPOINT_TEMPLATE 121 | discovery_endpoint = discovery_endpoint.replace('{authorize_host}', authority_host) 122 | discovery_endpoint = discovery_endpoint.replace('{authorize_endpoint}', 123 | quote(self._create_authority_url(), 124 | safe='~()*!.\'')) 125 | return urlparse(discovery_endpoint) 126 | 127 | def _perform_dynamic_instance_discovery(self): 128 | discovery_endpoint = self._create_instance_discovery_endpoint_from_template( 129 | AADConstants.WORLD_WIDE_AUTHORITY) 130 | get_options = util.create_request_options(self) 131 | operation = "Instance Discovery" 132 | self._log.debug("Attempting instance discover at: %(discovery_endpoint)s", 133 | {"discovery_endpoint": discovery_endpoint.geturl()}) 134 | 135 | try: 136 | resp = requests.get(discovery_endpoint.geturl(), headers=get_options['headers'], 137 | verify=self._call_context.get('verify_ssl', None), 138 | proxies=self._call_context.get('proxies', None)) 139 | util.log_return_correlation_id(self._log, operation, resp) 140 | except Exception: 141 | self._log.exception("%(operation)s request failed", 142 | {"operation": operation}) 143 | raise 144 | 145 | if resp.status_code == 429: 146 | resp.raise_for_status() # Will raise requests.exceptions.HTTPError 147 | if not util.is_http_success(resp.status_code): 148 | return_error_string = u"{} request returned http error: {}".format(operation, 149 | resp.status_code) 150 | error_response = "" 151 | if resp.text: 152 | return_error_string = u"{} and server response: {}".format(return_error_string, 153 | resp.text) 154 | try: 155 | error_response = resp.json() 156 | except ValueError: 157 | pass 158 | 159 | raise AdalError(return_error_string, error_response) 160 | 161 | else: 162 | discovery_resp = resp.json() 163 | if discovery_resp.get('tenant_discovery_endpoint'): 164 | return discovery_resp['tenant_discovery_endpoint'] 165 | else: 166 | raise AdalError('Failed to parse instance discovery response') 167 | 168 | def _validate_via_instance_discovery(self): 169 | valid = self._perform_static_instance_discovery() 170 | if not valid: 171 | self._perform_dynamic_instance_discovery() 172 | 173 | def _get_oauth_endpoints(self): 174 | 175 | if (not self.token_endpoint) or (not self.device_code_endpoint): 176 | self.token_endpoint = self._url.geturl() + AADConstants.TOKEN_ENDPOINT_PATH 177 | self.device_code_endpoint = self._url.geturl() + AADConstants.DEVICE_ENDPOINT_PATH 178 | 179 | def validate(self, call_context): 180 | 181 | self._log = log.Logger('Authority', call_context['log_context']) 182 | self._call_context = call_context 183 | 184 | if not self._validated: 185 | self._log.debug("Performing instance discovery: %(authority)s", 186 | {"authority": self._url.geturl()}) 187 | self._validate_via_instance_discovery() 188 | self._validated = True 189 | else: 190 | self._log.debug( 191 | "Instance discovery/validation has either already been completed or is turned off: %(authority)s", 192 | {"authority": self._url.geturl()}) 193 | 194 | self._get_oauth_endpoints() 195 | -------------------------------------------------------------------------------- /adal/code_request.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | from . import constants 29 | from . import log 30 | from . import oauth2_client 31 | 32 | OAUTH2_PARAMETERS = constants.OAuth2.Parameters 33 | 34 | class CodeRequest(object): 35 | def __init__(self, call_context, authentication_context, client_id, 36 | resource): 37 | self._log = log.Logger("CodeRequest", call_context['log_context']) 38 | self._call_context = call_context 39 | self._authentication_context = authentication_context 40 | self._client_id = client_id 41 | self._resource = resource 42 | 43 | def _get_user_code_info(self, oauth_parameters): 44 | client = self._create_oauth2_client() 45 | return client.get_user_code_info(oauth_parameters) 46 | 47 | def _create_oauth2_client(self): 48 | return oauth2_client.OAuth2Client( 49 | self._call_context, 50 | self._authentication_context.authority) 51 | 52 | def _create_oauth_parameters(self): 53 | return { 54 | OAUTH2_PARAMETERS.CLIENT_ID: self._client_id, 55 | OAUTH2_PARAMETERS.RESOURCE: self._resource 56 | } 57 | 58 | def get_user_code_info(self, language): 59 | self._log.info('Getting user code info.') 60 | 61 | oauth_parameters = self._create_oauth_parameters() 62 | if language: 63 | oauth_parameters[OAUTH2_PARAMETERS.LANGUAGE] = language 64 | 65 | return self._get_user_code_info(oauth_parameters) 66 | -------------------------------------------------------------------------------- /adal/constants.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | # pylint: disable=too-few-public-methods,old-style-class,no-init 28 | 29 | class Errors: 30 | # Constants 31 | ERROR_VALUE_NONE = '{} should not be None.' 32 | ERROR_VALUE_EMPTY_STRING = '{} should not be "".' 33 | ERROR_RESPONSE_MALFORMED_XML = 'The provided response string is not well formed XML.' 34 | 35 | class OAuth2Parameters(object): 36 | 37 | GRANT_TYPE = 'grant_type' 38 | CLIENT_ASSERTION = 'client_assertion' 39 | CLIENT_ASSERTION_TYPE = 'client_assertion_type' 40 | CLIENT_ID = 'client_id' 41 | CLIENT_SECRET = 'client_secret' 42 | REDIRECT_URI = 'redirect_uri' 43 | RESOURCE = 'resource' 44 | CODE = 'code' 45 | CODE_VERIFIER = 'code_verifier' 46 | SCOPE = 'scope' 47 | ASSERTION = 'assertion' 48 | AAD_API_VERSION = 'api-version' 49 | USERNAME = 'username' 50 | PASSWORD = 'password' 51 | REFRESH_TOKEN = 'refresh_token' 52 | LANGUAGE = 'mkt' 53 | DEVICE_CODE = 'device_code' 54 | 55 | class OAuth2GrantType(object): 56 | 57 | AUTHORIZATION_CODE = 'authorization_code' 58 | REFRESH_TOKEN = 'refresh_token' 59 | CLIENT_CREDENTIALS = 'client_credentials' 60 | JWT_BEARER = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' 61 | PASSWORD = 'password' 62 | SAML1 = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer' 63 | SAML2 = 'urn:ietf:params:oauth:grant-type:saml2-bearer' 64 | DEVICE_CODE = 'device_code' 65 | 66 | 67 | class OAuth2ResponseParameters(object): 68 | 69 | CODE = 'code' 70 | TOKEN_TYPE = 'token_type' 71 | ACCESS_TOKEN = 'access_token' 72 | ID_TOKEN = 'id_token' 73 | REFRESH_TOKEN = 'refresh_token' 74 | CREATED_ON = 'created_on' 75 | EXPIRES_ON = 'expires_on' 76 | EXPIRES_IN = 'expires_in' 77 | RESOURCE = 'resource' 78 | ERROR = 'error' 79 | ERROR_DESCRIPTION = 'error_description' 80 | 81 | class OAuth2DeviceCodeResponseParameters: 82 | USER_CODE = 'user_code' 83 | DEVICE_CODE = 'device_code' 84 | VERIFICATION_URL = 'verification_url' 85 | EXPIRES_IN = 'expires_in' 86 | INTERVAL = 'interval' 87 | MESSAGE = 'message' 88 | ERROR = 'error' 89 | ERROR_DESCRIPTION = 'error_description' 90 | 91 | class OAuth2Scope(object): 92 | 93 | OPENID = 'openid' 94 | 95 | 96 | class OAuth2(object): 97 | 98 | Parameters = OAuth2Parameters() 99 | GrantType = OAuth2GrantType() 100 | ResponseParameters = OAuth2ResponseParameters() 101 | DeviceCodeResponseParameters = OAuth2DeviceCodeResponseParameters() 102 | Scope = OAuth2Scope() 103 | IdTokenMap = { 104 | 'tid' : 'tenantId', 105 | 'given_name' : 'givenName', 106 | 'family_name' : 'familyName', 107 | 'idp' : 'identityProvider', 108 | 'oid' : 'oid' 109 | } 110 | 111 | 112 | class TokenResponseFields(object): 113 | 114 | TOKEN_TYPE = 'tokenType' 115 | ACCESS_TOKEN = 'accessToken' 116 | REFRESH_TOKEN = 'refreshToken' 117 | CREATED_ON = 'createdOn' 118 | EXPIRES_ON = 'expiresOn' 119 | EXPIRES_IN = 'expiresIn' 120 | RESOURCE = 'resource' 121 | USER_ID = 'userId' 122 | ERROR = 'error' 123 | ERROR_DESCRIPTION = 'errorDescription' 124 | 125 | # not from the wire, but amends for token cache 126 | _AUTHORITY = '_authority' 127 | _CLIENT_ID = '_clientId' 128 | IS_MRRT = 'isMRRT' 129 | 130 | 131 | class IdTokenFields(object): 132 | 133 | USER_ID = 'userId' 134 | IS_USER_ID_DISPLAYABLE = 'isUserIdDisplayable' 135 | TENANT_ID = 'tenantId' 136 | GIVE_NAME = 'givenName' 137 | FAMILY_NAME = 'familyName' 138 | IDENTITY_PROVIDER = 'identityProvider' 139 | 140 | class Misc(object): 141 | 142 | MAX_DATE = 0xffffffff 143 | CLOCK_BUFFER = 5 # In minutes. 144 | 145 | 146 | class Jwt(object): 147 | 148 | SELF_SIGNED_JWT_LIFETIME = 10 # 10 mins in mins 149 | AUDIENCE = 'aud' 150 | ISSUER = 'iss' 151 | SUBJECT = 'sub' 152 | NOT_BEFORE = 'nbf' 153 | EXPIRES_ON = 'exp' 154 | JWT_ID = 'jti' 155 | 156 | 157 | class UserRealm(object): 158 | 159 | federation_protocol_type = { 160 | 'WSFederation' : 'wstrust', 161 | 'SAML2' : 'saml20', 162 | 'Unknown' : 'unknown' 163 | } 164 | 165 | account_type = { 166 | 'Federated' : 'federated', 167 | 'Managed' : 'managed', 168 | 'Unknown' : 'unknown' 169 | } 170 | 171 | 172 | class Saml(object): 173 | 174 | TokenTypeV1 = 'urn:oasis:names:tc:SAML:1.0:assertion' 175 | TokenTypeV2 = 'urn:oasis:names:tc:SAML:2.0:assertion' 176 | OasisWssSaml11TokenProfile11 = "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" 177 | OasisWssSaml2TokenProfile2 = "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" 178 | 179 | 180 | class XmlNamespaces(object): 181 | namespaces = { 182 | 'wsdl' :'http://schemas.xmlsoap.org/wsdl/', 183 | 'sp' :'http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702', 184 | 'sp2005' :'http://schemas.xmlsoap.org/ws/2005/07/securitypolicy', 185 | 'wsu' :'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd', 186 | 'wsa10' :'http://www.w3.org/2005/08/addressing', 187 | 'http' :'http://schemas.microsoft.com/ws/06/2004/policy/http', 188 | 'soap12' :'http://schemas.xmlsoap.org/wsdl/soap12/', 189 | 'wsp' :'http://schemas.xmlsoap.org/ws/2004/09/policy', 190 | 's' :'http://www.w3.org/2003/05/soap-envelope', 191 | 'wsa' :'http://www.w3.org/2005/08/addressing', 192 | 'wst' :'http://docs.oasis-open.org/ws-sx/ws-trust/200512', 193 | 'trust' : "http://docs.oasis-open.org/ws-sx/ws-trust/200512", 194 | 'saml' : "urn:oasis:names:tc:SAML:1.0:assertion", 195 | 't' : 'http://schemas.xmlsoap.org/ws/2005/02/trust' 196 | } 197 | 198 | 199 | class Cache(object): 200 | 201 | HASH_ALGORITHM = 'sha256' 202 | 203 | 204 | class HttpError(object): 205 | 206 | UNAUTHORIZED = 401 207 | 208 | 209 | class AADConstants(object): 210 | 211 | WORLD_WIDE_AUTHORITY = 'login.microsoftonline.com' 212 | WELL_KNOWN_AUTHORITY_HOSTS = [ 213 | 'login.windows.net', 214 | 'login.microsoftonline.com', 215 | 'login.chinacloudapi.cn', 216 | 'login.microsoftonline.us', 217 | 'login.microsoftonline.de', 218 | ] 219 | INSTANCE_DISCOVERY_ENDPOINT_TEMPLATE = 'https://{authorize_host}/common/discovery/instance?authorization_endpoint={authorize_endpoint}&api-version=1.0' # pylint: disable=invalid-name 220 | AUTHORIZE_ENDPOINT_PATH = '/oauth2/authorize' 221 | TOKEN_ENDPOINT_PATH = '/oauth2/token' 222 | DEVICE_ENDPOINT_PATH = '/oauth2/devicecode' 223 | 224 | 225 | class AdalIdParameters(object): 226 | 227 | SKU = 'x-client-SKU' 228 | VERSION = 'x-client-Ver' 229 | OS = 'x-client-OS' # pylint: disable=invalid-name 230 | CPU = 'x-client-CPU' 231 | PYTHON_SKU = 'Python' 232 | 233 | class WSTrustVersion(object): 234 | UNDEFINED = 'undefined' 235 | WSTRUST13 = 'wstrust13' 236 | WSTRUST2005 = 'wstrust2005' 237 | 238 | -------------------------------------------------------------------------------- /adal/log.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import logging 29 | import uuid 30 | import traceback 31 | 32 | ADAL_LOGGER_NAME = 'adal-python' 33 | 34 | def create_log_context(correlation_id=None, enable_pii=False): 35 | return { 36 | 'correlation_id' : correlation_id or str(uuid.uuid4()), 37 | 'enable_pii': enable_pii} 38 | 39 | def set_logging_options(options=None): 40 | '''Configure adal logger, including level and handler spec'd by python 41 | logging module. 42 | 43 | Basic Usages:: 44 | >>>adal.set_logging_options({ 45 | >>> 'level': 'DEBUG', 46 | >>> 'handler': logging.FileHandler('adal.log') 47 | >>>}) 48 | ''' 49 | if options is None: 50 | options = {} 51 | logger = logging.getLogger(ADAL_LOGGER_NAME) 52 | 53 | logger.setLevel(options.get('level', logging.ERROR)) 54 | 55 | handler = options.get('handler') 56 | if handler: 57 | handler.setLevel(logger.level) 58 | logger.addHandler(handler) 59 | 60 | def get_logging_options(): 61 | '''Get logging options 62 | 63 | :returns: a dict, with a key of 'level' for logging level. 64 | ''' 65 | logger = logging.getLogger(ADAL_LOGGER_NAME) 66 | level = logger.getEffectiveLevel() 67 | return { 68 | 'level': logging.getLevelName(level) 69 | } 70 | 71 | class Logger(object): 72 | '''wrapper around python built-in logging to log correlation_id, and stack 73 | trace through keyword argument of 'log_stack_trace' 74 | ''' 75 | def __init__(self, component_name, log_context): 76 | 77 | if not log_context: 78 | raise AttributeError('Logger: log_context is a required parameter') 79 | 80 | self._component_name = component_name 81 | self.log_context = log_context 82 | self._logging = logging.getLogger(ADAL_LOGGER_NAME) 83 | 84 | def _log_message(self, msg, log_stack_trace=None): 85 | correlation_id = self.log_context.get("correlation_id", 86 | "") 87 | 88 | formatted = "{} - {}:{}".format( 89 | correlation_id, 90 | self._component_name, 91 | msg) 92 | if log_stack_trace: 93 | formatted += "\nStack:\n{}".format(traceback.format_stack()) 94 | 95 | return formatted 96 | 97 | def warn(self, msg, *args, **kwargs): 98 | """ 99 | The recommended way to call this function with variable content, 100 | is to use the `warn("hello %(name)s", {"name": "John Doe"}` form, 101 | so that this method will scrub pii value when needed. 102 | """ 103 | if len(args) == 1 and isinstance(args[0], dict) and not self.log_context.get('enable_pii'): 104 | args = (scrub_pii(args[0]),) 105 | log_stack_trace = kwargs.pop('log_stack_trace', None) 106 | msg = self._log_message(msg, log_stack_trace) 107 | self._logging.warning(msg, *args, **kwargs) 108 | 109 | def info(self, msg, *args, **kwargs): 110 | if len(args) == 1 and isinstance(args[0], dict) and not self.log_context.get('enable_pii'): 111 | args = (scrub_pii(args[0]),) 112 | log_stack_trace = kwargs.pop('log_stack_trace', None) 113 | msg = self._log_message(msg, log_stack_trace) 114 | self._logging.info(msg, *args, **kwargs) 115 | 116 | def debug(self, msg, *args, **kwargs): 117 | if len(args) == 1 and isinstance(args[0], dict) and not self.log_context.get('enable_pii'): 118 | args = (scrub_pii(args[0]),) 119 | log_stack_trace = kwargs.pop('log_stack_trace', None) 120 | msg = self._log_message(msg, log_stack_trace) 121 | self._logging.debug(msg, *args, **kwargs) 122 | 123 | def exception(self, msg, *args, **kwargs): 124 | if len(args) == 1 and isinstance(args[0], dict) and not self.log_context.get('enable_pii'): 125 | args = (scrub_pii(args[0]),) 126 | msg = self._log_message(msg) 127 | self._logging.exception(msg, *args, **kwargs) 128 | 129 | 130 | def scrub_pii(arg_dict, padding="..."): 131 | """ 132 | The input is a dict with semantic keys, 133 | and the output will be a dict with PII values replaced by padding. 134 | """ 135 | pii = set([ # Personally Identifiable Information 136 | "subject", 137 | "upn", # i.e. user name 138 | "given_name", "family_name", 139 | "email", 140 | "oid", # Object ID 141 | "userid", # Used in ADAL Python token cache 142 | "login_hint", 143 | "home_oid", 144 | "access_token", "refresh_token", "id_token", "token_response", 145 | 146 | # The following are actually Organizationally Identifiable Info 147 | "tenant_id", 148 | "authority", # which typically contains tenant_id 149 | "client_id", 150 | "_clientid", # This is the key name ADAL uses in cache query 151 | "redirect_uri", 152 | 153 | # Unintuitively, the following can contain PII 154 | "user_realm_url", # e.g. https://login.microsoftonline.com/common/UserRealm/{username} 155 | ]) 156 | return {k: padding if k.lower() in pii else arg_dict[k] for k in arg_dict} 157 | 158 | -------------------------------------------------------------------------------- /adal/self_signed_jwt.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import time 29 | import datetime 30 | import uuid 31 | import base64 32 | import binascii 33 | import re 34 | 35 | import jwt 36 | 37 | from .constants import Jwt 38 | from .log import Logger 39 | from .adal_error import AdalError 40 | 41 | def _get_date_now(): 42 | return datetime.datetime.now() 43 | 44 | def _get_new_jwt_id(): 45 | return str(uuid.uuid4()) 46 | 47 | def _create_x5t_value(thumbprint): 48 | hex_val = binascii.a2b_hex(thumbprint) 49 | return base64.urlsafe_b64encode(hex_val).decode() 50 | 51 | def _sign_jwt(header, payload, certificate): 52 | try: 53 | encoded_jwt = _encode_jwt(payload, certificate, header) 54 | except Exception as exp: 55 | raise AdalError("Error:Invalid Certificate: Expected Start of Certificate to be '-----BEGIN RSA PRIVATE KEY-----'", exp) 56 | _raise_on_invalid_jwt_signature(encoded_jwt) 57 | return encoded_jwt 58 | 59 | def _encode_jwt(payload, certificate, header): 60 | encoded = jwt.encode(payload, certificate, algorithm='RS256', headers=header) 61 | try: 62 | return encoded.decode() # PyJWT 1.x returns bytes; historically we convert it to string 63 | except AttributeError: 64 | return encoded # PyJWT 2 will return string 65 | 66 | def _raise_on_invalid_jwt_signature(encoded_jwt): 67 | segments = encoded_jwt.split('.') 68 | if len(segments) < 3 or not segments[2]: 69 | raise AdalError('Failed to sign JWT. This is most likely due to an invalid certificate.') 70 | 71 | def _extract_certs(public_cert_content): 72 | # Parses raw public certificate file contents and returns a list of strings 73 | # Usage: headers = {"x5c": extract_certs(open("my_cert.pem").read())} 74 | public_certificates = re.findall( 75 | r'-----BEGIN CERTIFICATE-----(?P[^-]+)-----END CERTIFICATE-----', 76 | public_cert_content, re.I) 77 | if public_certificates: 78 | return [cert.strip() for cert in public_certificates] 79 | # The public cert tags are not found in the input, 80 | # let's make best effort to exclude a private key pem file. 81 | if "PRIVATE KEY" in public_cert_content: 82 | raise ValueError( 83 | "We expect your public key but detect a private key instead") 84 | return [public_cert_content.strip()] 85 | 86 | class SelfSignedJwt(object): 87 | 88 | NumCharIn128BitHexString = 128/8*2 89 | numCharIn160BitHexString = 160/8*2 90 | ThumbprintRegEx = r"^[a-f\d]*$" 91 | 92 | def __init__(self, call_context, authority, client_id): 93 | self._log = Logger('SelfSignedJwt', call_context['log_context']) 94 | self._call_context = call_context 95 | 96 | self._authortiy = authority 97 | self._token_endpoint = authority.token_endpoint 98 | self._client_id = client_id 99 | 100 | def _create_header(self, thumbprint, public_certificate): 101 | x5t = _create_x5t_value(thumbprint) 102 | header = {'typ':'JWT', 'alg':'RS256', 'x5t':x5t} 103 | if public_certificate: 104 | header['x5c'] = _extract_certs(public_certificate) 105 | self._log.debug("Creating self signed JWT header. x5t: %(x5t)s, x5c: %(x5c)s", 106 | {"x5t": x5t, "x5c": public_certificate}) 107 | 108 | return header 109 | 110 | def _create_payload(self): 111 | now = _get_date_now() 112 | minutes = datetime.timedelta(0, 0, 0, 0, Jwt.SELF_SIGNED_JWT_LIFETIME) 113 | expires = now + minutes 114 | 115 | self._log.debug( 116 | 'Creating self signed JWT payload. Expires: %(expires)s NotBefore: %(nbf)s', 117 | {"expires": expires, "nbf": now}) 118 | 119 | jwt_payload = {} 120 | jwt_payload[Jwt.AUDIENCE] = self._token_endpoint 121 | jwt_payload[Jwt.ISSUER] = self._client_id 122 | jwt_payload[Jwt.SUBJECT] = self._client_id 123 | jwt_payload[Jwt.NOT_BEFORE] = int(time.mktime(now.timetuple())) 124 | jwt_payload[Jwt.EXPIRES_ON] = int(time.mktime(expires.timetuple())) 125 | jwt_payload[Jwt.JWT_ID] = _get_new_jwt_id() 126 | 127 | return jwt_payload 128 | 129 | def _raise_on_invalid_thumbprint(self, thumbprint): 130 | thumbprint_sizes = [self.NumCharIn128BitHexString, self.numCharIn160BitHexString] 131 | size_ok = len(thumbprint) in thumbprint_sizes 132 | if not size_ok or not re.search(self.ThumbprintRegEx, thumbprint): 133 | raise AdalError("The thumbprint does not match a known format") 134 | 135 | def _reduce_thumbprint(self, thumbprint): 136 | canonical = thumbprint.lower().replace(' ', '').replace(':', '') 137 | self._raise_on_invalid_thumbprint(canonical) 138 | return canonical 139 | 140 | def create(self, certificate, thumbprint, public_certificate): 141 | thumbprint = self._reduce_thumbprint(thumbprint) 142 | 143 | header = self._create_header(thumbprint, public_certificate) 144 | payload = self._create_payload() 145 | return _sign_jwt(header, payload, certificate) 146 | -------------------------------------------------------------------------------- /adal/token_cache.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import json 29 | import threading 30 | 31 | from .constants import TokenResponseFields 32 | 33 | def _string_cmp(str1, str2): 34 | '''Case insensitive comparison. Return true if both are None''' 35 | str1 = str1 if str1 is not None else '' 36 | str2 = str2 if str2 is not None else '' 37 | return str1.lower() == str2.lower() 38 | 39 | class TokenCacheKey(object): # pylint: disable=too-few-public-methods 40 | def __init__(self, authority, resource, client_id, user_id): 41 | self.authority = authority 42 | self.resource = resource 43 | self.client_id = client_id 44 | self.user_id = user_id 45 | 46 | def __hash__(self): 47 | return hash((self.authority, self.resource, self.client_id, self.user_id)) 48 | 49 | def __eq__(self, other): 50 | return _string_cmp(self.authority, other.authority) and \ 51 | _string_cmp(self.resource, other.resource) and \ 52 | _string_cmp(self.client_id, other.client_id) and \ 53 | _string_cmp(self.user_id, other.user_id) 54 | 55 | def __ne__(self, other): 56 | return not self == other 57 | 58 | # pylint: disable=protected-access 59 | 60 | def _get_cache_key(entry): 61 | return TokenCacheKey( 62 | entry.get(TokenResponseFields._AUTHORITY), 63 | entry.get(TokenResponseFields.RESOURCE), 64 | entry.get(TokenResponseFields._CLIENT_ID), 65 | entry.get(TokenResponseFields.USER_ID)) 66 | 67 | 68 | class TokenCache(object): 69 | def __init__(self, state=None): 70 | self._cache = {} 71 | self._lock = threading.RLock() 72 | if state: 73 | self.deserialize(state) 74 | self.has_state_changed = False 75 | 76 | def find(self, query): 77 | with self._lock: 78 | return self._query_cache( 79 | query.get(TokenResponseFields.IS_MRRT), 80 | query.get(TokenResponseFields.USER_ID), 81 | query.get(TokenResponseFields._CLIENT_ID)) 82 | 83 | def remove(self, entries): 84 | with self._lock: 85 | for e in entries: 86 | key = _get_cache_key(e) 87 | removed = self._cache.pop(key, None) 88 | if removed is not None: 89 | self.has_state_changed = True 90 | 91 | def add(self, entries): 92 | with self._lock: 93 | for e in entries: 94 | key = _get_cache_key(e) 95 | self._cache[key] = e 96 | self.has_state_changed = True 97 | 98 | def serialize(self): 99 | with self._lock: 100 | return json.dumps(list(self._cache.values())) 101 | 102 | def deserialize(self, state): 103 | with self._lock: 104 | self._cache.clear() 105 | if state: 106 | tokens = json.loads(state) 107 | for t in tokens: 108 | key = _get_cache_key(t) 109 | self._cache[key] = t 110 | 111 | def read_items(self): 112 | '''output list of tuples in (key, authentication-result)''' 113 | with self._lock: 114 | return self._cache.items() 115 | 116 | def _query_cache(self, is_mrrt, user_id, client_id): 117 | matches = [] 118 | for k in self._cache: 119 | v = self._cache[k] 120 | #None value will be taken as wildcard match 121 | #pylint: disable=too-many-boolean-expressions 122 | if ((is_mrrt is None or is_mrrt == v.get(TokenResponseFields.IS_MRRT)) and 123 | (user_id is None or _string_cmp(user_id, v.get(TokenResponseFields.USER_ID))) and 124 | (client_id is None or _string_cmp(client_id, v.get(TokenResponseFields._CLIENT_ID)))): 125 | matches.append(v) 126 | return matches 127 | -------------------------------------------------------------------------------- /adal/user_realm.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | import json 28 | 29 | try: 30 | from urllib.parse import quote, urlencode 31 | from urllib.parse import urlunparse 32 | except ImportError: 33 | from urllib import quote, urlencode #pylint: disable=no-name-in-module 34 | from urlparse import urlunparse #pylint: disable=import-error 35 | 36 | import requests 37 | 38 | from . import constants 39 | from . import log 40 | from . import util 41 | from .adal_error import AdalError 42 | 43 | USER_REALM_PATH_TEMPLATE = 'common/UserRealm/' 44 | 45 | ACCOUNT_TYPE = constants.UserRealm.account_type 46 | FEDERATION_PROTOCOL_TYPE = constants.UserRealm.federation_protocol_type 47 | 48 | 49 | class UserRealm(object): 50 | 51 | def __init__(self, call_context, user_principle, authority_url): 52 | 53 | self._log = log.Logger("UserRealm", call_context['log_context']) 54 | self._call_context = call_context 55 | self.api_version = '1.0' 56 | self.federation_protocol = None 57 | self.account_type = None 58 | self.federation_metadata_url = None 59 | self.federation_active_auth_url = None 60 | self.cloud_audience_urn = None 61 | self._user_principle = user_principle 62 | self._authority_url = authority_url 63 | 64 | def _get_user_realm_url(self): 65 | 66 | url_components = list(util.copy_url(self._authority_url)) 67 | url_encoded_user = quote(self._user_principle, safe='~()*!.\'') 68 | url_components[2] = '/' + USER_REALM_PATH_TEMPLATE.replace('', url_encoded_user) 69 | 70 | user_realm_query = {'api-version':self.api_version} 71 | url_components[4] = urlencode(user_realm_query) 72 | return util.copy_url(urlunparse(url_components)) 73 | 74 | @staticmethod 75 | def _validate_constant_value(value_dic, value, case_sensitive=False): 76 | 77 | if not value: 78 | return False 79 | 80 | if not case_sensitive: 81 | value = value.lower() 82 | 83 | return value if value in value_dic.values() else False 84 | 85 | @staticmethod 86 | def _validate_account_type(account_type): 87 | return UserRealm._validate_constant_value(ACCOUNT_TYPE, account_type) 88 | 89 | @staticmethod 90 | def _validate_federation_protocol(protocol): 91 | return UserRealm._validate_constant_value(FEDERATION_PROTOCOL_TYPE, protocol) 92 | 93 | def _log_parsed_response(self): 94 | 95 | self._log.debug( 96 | 'UserRealm response:\n' 97 | ' AccountType: %(account_type)s\n' 98 | ' FederationProtocol: %(federation_protocol)s\n' 99 | ' FederationMetatdataUrl: %(federation_metadata_url)s\n' 100 | ' FederationActiveAuthUrl: %(federation_active_auth_url)s', 101 | { 102 | "account_type": self.account_type, 103 | "federation_protocol": self.federation_protocol, 104 | "federation_metadata_url": self.federation_metadata_url, 105 | "federation_active_auth_url": self.federation_active_auth_url, 106 | }) 107 | 108 | def _parse_discovery_response(self, body): 109 | 110 | self._log.debug("Discovery response:\n %(discovery_response)s", 111 | {"discovery_response": body}) 112 | 113 | try: 114 | response = json.loads(body) 115 | except ValueError: 116 | self._log.info( 117 | "Parsing realm discovery response JSON failed for body: %(body)s", 118 | {"body": body}) 119 | raise 120 | 121 | account_type = UserRealm._validate_account_type(response['account_type']) 122 | if not account_type: 123 | raise AdalError('Cannot parse account_type: {}'.format(account_type)) 124 | self.account_type = account_type 125 | 126 | if self.account_type == ACCOUNT_TYPE['Federated']: 127 | protocol = UserRealm._validate_federation_protocol(response['federation_protocol']) 128 | 129 | if not protocol: 130 | raise AdalError('Cannot parse federation protocol: {}'.format(protocol)) 131 | 132 | self.federation_protocol = protocol 133 | self.federation_metadata_url = response['federation_metadata_url'] 134 | self.federation_active_auth_url = response['federation_active_auth_url'] 135 | self.cloud_audience_urn = response.get('cloud_audience_urn', "urn:federation:MicrosoftOnline") 136 | 137 | self._log_parsed_response() 138 | 139 | def discover(self): 140 | 141 | options = util.create_request_options(self, {'headers': {'Accept':'application/json'}}) 142 | user_realm_url = self._get_user_realm_url() 143 | self._log.debug("Performing user realm discovery at: %(user_realm_url)s", 144 | {"user_realm_url": user_realm_url.geturl()}) 145 | 146 | operation = 'User Realm Discovery' 147 | resp = requests.get(user_realm_url.geturl(), headers=options['headers'], 148 | proxies=self._call_context.get('proxies', None), 149 | verify=self._call_context.get('verify_ssl', None)) 150 | util.log_return_correlation_id(self._log, operation, resp) 151 | 152 | if resp.status_code == 429: 153 | resp.raise_for_status() # Will raise requests.exceptions.HTTPError 154 | if not util.is_http_success(resp.status_code): 155 | return_error_string = u"{} request returned http error: {}".format(operation, 156 | resp.status_code) 157 | error_response = "" 158 | if resp.text: 159 | return_error_string = u"{} and server response: {}".format(return_error_string, resp.text) 160 | try: 161 | error_response = resp.json() 162 | except ValueError: 163 | pass 164 | 165 | raise AdalError(return_error_string, error_response) 166 | 167 | else: 168 | self._parse_discovery_response(resp.text) 169 | -------------------------------------------------------------------------------- /adal/util.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import sys 29 | import base64 30 | try: 31 | from urllib.parse import urlparse 32 | except ImportError: 33 | from urlparse import urlparse #pylint: disable=import-error 34 | 35 | import adal 36 | 37 | from .constants import AdalIdParameters 38 | 39 | def is_http_success(status_code): 40 | return status_code >= 200 and status_code < 300 41 | 42 | def add_default_request_headers(self, options): 43 | if not options.get('headers'): 44 | options['headers'] = {} 45 | 46 | headers = options['headers'] 47 | if not headers.get('Accept-Charset'): 48 | headers['Accept-Charset'] = 'utf-8' 49 | 50 | #pylint: disable=protected-access 51 | headers['client-request-id'] = self._call_context['log_context']['correlation_id'] 52 | headers['return-client-request-id'] = 'true' 53 | 54 | headers[AdalIdParameters.SKU] = AdalIdParameters.PYTHON_SKU 55 | headers[AdalIdParameters.VERSION] = adal.__version__ 56 | headers[AdalIdParameters.OS] = sys.platform 57 | headers[AdalIdParameters.CPU] = 'x64' if sys.maxsize > 2 ** 32 else 'x86' 58 | 59 | def create_request_options(self, *options): 60 | 61 | merged_options = {} 62 | 63 | if options: 64 | for i in options: 65 | merged_options.update(i) 66 | 67 | #pylint: disable=protected-access 68 | if self._call_context.get('options') and self._call_context['options'].get('http'): 69 | merged_options.update(self._call_context['options']['http']) 70 | 71 | add_default_request_headers(self, merged_options) 72 | return merged_options 73 | 74 | 75 | def log_return_correlation_id(log, operation_message, response): 76 | if response and response.headers and response.headers.get('client-request-id'): 77 | log.debug("{} Server returned this correlation_id: {}".format( 78 | operation_message, 79 | response.headers['client-request-id'])) 80 | 81 | def copy_url(url_source): 82 | if hasattr(url_source, 'geturl'): 83 | return urlparse(url_source.geturl()) 84 | else: 85 | return urlparse(url_source) 86 | 87 | # urlsafe_b64decode requires correct padding. AAD does not include padding so 88 | # the string needs to be correctly padded before decoding. 89 | def base64_urlsafe_decode(b64string): 90 | b64string += '=' * (4 - ((len(b64string) % 4))) 91 | return base64.urlsafe_b64decode(b64string.encode('ascii')) 92 | 93 | -------------------------------------------------------------------------------- /adal/xmlutil.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | try: 29 | from xml.etree import cElementTree as ET 30 | except ImportError: 31 | from xml.etree import ElementTree as ET 32 | 33 | from . import constants 34 | 35 | XPATH_PATH_TEMPLATE = '*[local-name() = \'LOCAL_NAME\' and namespace-uri() = \'NAMESPACE\']' 36 | 37 | def expand_q_names(xpath): 38 | 39 | namespaces = constants.XmlNamespaces.namespaces 40 | path_parts = xpath.split('/') 41 | for index, part in enumerate(path_parts): 42 | if part.find(":") != -1: 43 | q_parts = part.split(':') 44 | if len(q_parts) != 2: 45 | raise IndexError("Unable to parse XPath string: {} with QName: {}".format(xpath, part)) 46 | 47 | expanded_path = XPATH_PATH_TEMPLATE.replace('LOCAL_NAME', q_parts[1]) 48 | expanded_path = expanded_path.replace('NAMESPACE', namespaces[q_parts[0]]) 49 | path_parts[index] = expanded_path 50 | 51 | return '/'.join(path_parts) 52 | 53 | def xpath_find(dom, xpath): 54 | return dom.findall(xpath, constants.XmlNamespaces.namespaces) 55 | 56 | def serialize_node_children(node): 57 | 58 | doc = "" 59 | for child in node.iter(): 60 | if is_element_node(child): 61 | estring = ET.tostring(child) 62 | doc += estring if isinstance(estring, str) else estring.decode() 63 | 64 | return doc if doc else None 65 | 66 | def is_element_node(node): 67 | return hasattr(node, 'tag') 68 | 69 | def find_element_text(node): 70 | 71 | for child in node.iter(): 72 | if child.text: 73 | return child.text 74 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | Azure Active Directory SDK projects welcomes new contributors. This document will guide you 4 | through the process. 5 | 6 | ### CONTRIBUTOR LICENSE AGREEMENT 7 | 8 | Please visit [https://cla.microsoft.com/](https://cla.microsoft.com/) and sign the Contributor License 9 | Agreement. You only need to do that once. We can not look at your code until you've submitted this request. 10 | 11 | 12 | ### FORK 13 | 14 | Fork the project [on GitHub][] and check out 15 | your copy. 16 | 17 | Example for ADAL Python: 18 | 19 | ``` 20 | $ git clone git@github.com:username/azure-activedirectory-library-for-python.git 21 | $ cd azure-activedirectory-library-for-python 22 | $ git remote add upstream git@github.com:AzureAD/azure-activedirectory-library-for-python.git 23 | ``` 24 | 25 | Now decide if you want your feature or bug fix to go into the dev branch 26 | or the master branch. **All bug fixes and new features should go into the dev branch.** 27 | 28 | The master branch is effectively frozen; patches that change the SDKs 29 | protocols or API surface area or affect the run-time behavior of the SDK will be rejected. 30 | 31 | Some of our SDKs have bundled dependencies that are not part of the project proper. Any changes to files in those directories or its subdirectories should be sent to their respective 32 | projects. Do not send your patch to us, we cannot accept it. 33 | 34 | In case of doubt, open an issue in the [issue tracker][]. 35 | 36 | Especially do so if you plan to work on a major change in functionality. Nothing is more 37 | frustrating than seeing your hard work go to waste because your vision 38 | does not align with our goals for the SDK. 39 | 40 | 41 | ### BRANCH 42 | 43 | Okay, so you have decided on the proper branch. Create a feature branch 44 | and start hacking: 45 | 46 | ``` 47 | $ git checkout -b my-feature-branch 48 | ``` 49 | 50 | ### COMMIT 51 | 52 | Make sure git knows your name and email address: 53 | 54 | ``` 55 | $ git config --global user.name "J. Random User" 56 | $ git config --global user.email "j.random.user@example.com" 57 | ``` 58 | 59 | Writing good commit logs is important. A commit log should describe what 60 | changed and why. Follow these guidelines when writing one: 61 | 62 | 1. The first line should be 50 characters or less and contain a short 63 | description of the change prefixed with the name of the changed 64 | subsystem (e.g. "net: add localAddress and localPort to Socket"). 65 | 2. Keep the second line blank. 66 | 3. Wrap all other lines at 72 columns. 67 | 68 | A good commit log looks like this: 69 | 70 | ``` 71 | fix: explaining the commit in one line 72 | 73 | Body of commit message is a few lines of text, explaining things 74 | in more detail, possibly giving some background about the issue 75 | being fixed, etc etc. 76 | 77 | The body of the commit message can be several paragraphs, and 78 | please do proper word-wrap and keep columns shorter than about 79 | 72 characters or so. That way `git log` will show things 80 | nicely even when it is indented. 81 | ``` 82 | 83 | The header line should be meaningful; it is what other people see when they 84 | run `git shortlog` or `git log --oneline`. 85 | 86 | Check the output of `git log --oneline files_that_you_changed` to find out 87 | what directories your changes touch. 88 | 89 | 90 | ### REBASE 91 | 92 | Use `git rebase` (not `git merge`) to sync your work from time to time. 93 | 94 | ``` 95 | $ git fetch upstream 96 | $ git rebase upstream/v0.1 # or upstream/master 97 | ``` 98 | 99 | 100 | ### TEST 101 | 102 | Bug fixes and features should come with tests. Add your tests in the 103 | test directory. This varies by repository but often follows the same convention of /src/test. Look at other tests to see how they should be 104 | structured (license boilerplate, common includes, etc.). 105 | 106 | 107 | Make sure that all tests pass. 108 | 109 | 110 | ### PUSH 111 | 112 | ``` 113 | $ git push origin my-feature-branch 114 | ``` 115 | 116 | Go to https://github.com/username/azure-activedirectory-library-for-***.git and select your feature branch. Click 117 | the 'Pull Request' button and fill out the form. 118 | 119 | Pull requests are usually reviewed within a few days. If there are comments 120 | to address, apply your changes in a separate commit and push that to your 121 | feature branch. Post a comment in the pull request afterwards; GitHub does 122 | not send out notifications when you add commits. 123 | 124 | 125 | [on GitHub]: https://github.com/AzureAD/azure-activedirectory-library-for-python 126 | [issue tracker]: https://github.com/AzureAD/azure-activedirectory-library-for-python/issues 127 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = ADALPython 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=ADALPython 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzureAD/azure-activedirectory-library-for-python/e546d35f2cc2e41d89ead7e37cfa3e59b7c3226b/docs/requirements.txt -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # ADAL Python documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Apr 24 14:09:40 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.autodoc', 35 | 'sphinx.ext.doctest', 36 | 'sphinx.ext.intersphinx', 37 | 'sphinx.ext.todo', 38 | 'sphinx.ext.coverage', 39 | 'sphinx.ext.imgmath', 40 | 'sphinx.ext.ifconfig', 41 | 'sphinx.ext.viewcode', 42 | 'sphinx.ext.githubpages'] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = '.rst' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = 'ADAL Python' 58 | copyright = '2018, Microsoft' 59 | author = 'Microsoft' 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = '' 67 | # The full version, including alpha/beta/rc tags. 68 | release = '' 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = None 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | # This patterns also effect to html_static_path and html_extra_path 80 | exclude_patterns = [] 81 | 82 | # The name of the Pygments (syntax highlighting) style to use. 83 | pygments_style = 'sphinx' 84 | 85 | # If true, `todo` and `todoList` produce output, else they produce nothing. 86 | todo_include_todos = True 87 | 88 | # -- Options for HTML output ---------------------------------------------- 89 | 90 | # The theme to use for HTML and HTML Help pages. See the documentation for 91 | # a list of builtin themes. 92 | # 93 | html_theme = 'sphinx_rtd_theme' 94 | 95 | # Theme options are theme-specific and customize the look and feel of a theme 96 | # further. For a list of options available for each theme, see the 97 | # documentation. 98 | # 99 | # html_theme_options = {} 100 | 101 | # Add any paths that contain custom static files (such as style sheets) here, 102 | # relative to this directory. They are copied after the builtin static files, 103 | # so a file named "default.css" will overwrite the builtin "default.css". 104 | html_static_path = ['_static'] 105 | 106 | # Custom sidebar templates, must be a dictionary that maps document names 107 | # to template names. 108 | # 109 | # This is required for the alabaster theme 110 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 111 | html_sidebars = { 112 | '**': [ 113 | 'relations.html', # needs 'show_related': True theme option to display 114 | 'searchbox.html', 115 | ] 116 | } 117 | 118 | # -- Options for HTMLHelp output ------------------------------------------ 119 | 120 | # Output file base name for HTML help builder. 121 | htmlhelp_basename = 'ADALPythondoc' 122 | 123 | # -- Options for LaTeX output --------------------------------------------- 124 | 125 | latex_elements = { 126 | # The paper size ('letterpaper' or 'a4paper'). 127 | # 128 | # 'papersize': 'letterpaper', 129 | 130 | # The font size ('10pt', '11pt' or '12pt'). 131 | # 132 | # 'pointsize': '10pt', 133 | 134 | # Additional stuff for the LaTeX preamble. 135 | # 136 | # 'preamble': '', 137 | 138 | # Latex figure (float) alignment 139 | # 140 | # 'figure_align': 'htbp', 141 | } 142 | 143 | # Grouping the document tree into LaTeX files. List of tuples 144 | # (source start file, target name, title, 145 | # author, documentclass [howto, manual, or own class]). 146 | latex_documents = [ 147 | (master_doc, 'ADALPython.tex', 'ADAL Python Documentation', 148 | 'Microsoft', 'manual'), 149 | ] 150 | 151 | # -- Options for manual page output --------------------------------------- 152 | 153 | # One entry per manual page. List of tuples 154 | # (source start file, name, description, authors, manual section). 155 | man_pages = [ 156 | (master_doc, 'adalpython', 'ADAL Python Documentation', 157 | [author], 1) 158 | ] 159 | 160 | # -- Options for Texinfo output ------------------------------------------- 161 | 162 | # Grouping the document tree into Texinfo files. List of tuples 163 | # (source start file, target name, title, author, 164 | # dir menu entry, description, category) 165 | texinfo_documents = [ 166 | (master_doc, 'ADALPython', 'ADAL Python Documentation', 167 | author, 'ADALPython', 'One line description of project.', 168 | 'Miscellaneous'), 169 | ] 170 | 171 | # -- Options for Epub output ---------------------------------------------- 172 | 173 | # Bibliographic Dublin Core info. 174 | epub_title = project 175 | epub_author = author 176 | epub_publisher = author 177 | epub_copyright = copyright 178 | 179 | # The unique identifier of the text. This can be a ISBN number 180 | # or the project homepage. 181 | # 182 | # epub_identifier = '' 183 | 184 | # A unique identification for the text. 185 | # 186 | # epub_uid = '' 187 | 188 | # A list of files that should not be packed into the epub file. 189 | epub_exclude_files = ['search.html'] 190 | 191 | # Example configuration for intersphinx: refer to the Python standard library. 192 | intersphinx_mapping = {'https://docs.python.org/': None} 193 | autoclass_content = 'both' 194 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. ADAL Python documentation master file, created by 2 | sphinx-quickstart on Wed Apr 25 15:50:25 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. This file is also inspired by 7 | https://pythonhosted.org/an_example_pypi_project/sphinx.html#full-code-example 8 | 9 | .. note:: 10 | This library, ADAL for Python, will no longer receive new feature improvement. Its successor, 11 | `MSAL for Python `_, 12 | are now generally available. 13 | 14 | * If you are starting a new project, you can get started with the 15 | `MSAL Python docs `_ 16 | for details about the scenarios, usage, and relevant concepts. 17 | * If your application is using the previous ADAL Python library, you can follow this 18 | `migration guide `_ 19 | to update to MSAL Python. 20 | * Existing applications relying on ADAL Python will continue to work. 21 | 22 | 23 | Welcome to ADAL Python's documentation! 24 | ======================================= 25 | 26 | .. toctree:: 27 | :maxdepth: 2 28 | :caption: Contents: 29 | 30 | You can find high level conceptual documentations in the project 31 | `wiki `_ 32 | and 33 | `workable samples inside the project code base 34 | `_ 35 | 36 | The documentation hosted here is for API Reference. 37 | 38 | 39 | AuthenticationContext 40 | ===================== 41 | 42 | The majority of ADAL Python functionalities are provided via the main class 43 | named `AuthenticationContext`. 44 | 45 | .. autoclass:: adal.AuthenticationContext 46 | :members: 47 | 48 | .. automethod:: __init__ 49 | 50 | 51 | TokenCache 52 | ========== 53 | 54 | One of the parameter accepted by `AuthenticationContext` is the `TokenCache`. 55 | 56 | .. autoclass:: adal.TokenCache 57 | :members: 58 | :undoc-members: 59 | 60 | If you need to subclass it, you need to refer to its source code for the detail. 61 | 62 | 63 | AdalError 64 | ========= 65 | 66 | When errors are detected by ADAL Python, it will raise this exception. 67 | 68 | .. autoclass:: adal.AdalError 69 | :members: 70 | 71 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | ignore=.svn 4 | persistent=yes 5 | cache-size=500 6 | load-plugins= 7 | 8 | [MESSAGES CONTROL] 9 | 10 | #enable-checker= 11 | #disable-checker=design 12 | #enable-msg-cat= 13 | #disable-msg-cat= 14 | #enable-msg= 15 | 16 | # Disabled messages: 17 | # C0321: Multiple statements on a single line 18 | # W0105: String statement has no effect 19 | # W0142: Used * or ** magic 20 | # W0404: Reimport '' 21 | # W0704: Except doesn't do anything Used when an except clause does nothing but "pass" 22 | # and there is no "else" clause. 23 | # I0011: Locally disabling (message) 24 | # R0921: Abstract class not referenced 25 | # C0111: missing-docstring 26 | # C0303: railing whitespace 27 | # C0301: Line too long 28 | disable=C0111,C0321,C0303,C0301,W0105,W0142,W0404,W0704,I0011,R0921 29 | 30 | [REPORTS] 31 | 32 | # Available formats are text, parseable, colorized, msvs (Visual Studio) and html 33 | output-format=msvs 34 | files-output=no 35 | reports=yes 36 | 37 | # Python expression which should return a note less than 10 (10 is the highest 38 | # note).You have access to the variables errors warning, statement which 39 | # respectively contain the number of errors / warnings messages and the total 40 | # number of statements analyzed. This is used by the global evaluation report 41 | # (R0004). 42 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 43 | 44 | #enable-report= 45 | #disable-report= 46 | 47 | [BASIC] 48 | 49 | no-docstring-rgx=__.*__ 50 | 51 | # Regular expression which should only match correct module names 52 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 53 | 54 | # Regular expression which should only match correct module level names 55 | const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__)|([a-z_][a-z0-9_]*))$ 56 | 57 | # Regular expression which should only match correct class names 58 | class-rgx=[a-zA-Z0-9_]+$ 59 | 60 | # Regular expression which should only match correct function names 61 | function-rgx=[a-zA-Z_][a-zA-Z0-9_]{2,50}$ 62 | 63 | # Regular expression which should only match correct method names 64 | method-rgx=[a-z_][a-zA-Z0-9_]{2,60}$ 65 | 66 | # Regular expression which should only match correct instance attribute names 67 | attr-rgx=[a-z_][a-z0-9_]{1,30}$ 68 | 69 | # Regular expression which should only match correct argument names 70 | argument-rgx=[a-z_][a-z0-9_]{1,30}$ 71 | 72 | # Regular expression which should only match correct variable names 73 | variable-rgx=[a-z_][a-zA-Z0-9_]{0,40}$ 74 | 75 | # Regular expression which should only match correct list comprehension / 76 | # generator expression variable names 77 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 78 | 79 | # Good variable names which should always be accepted, separated by a comma 80 | good-names=i,j,k,ex,Run,_,x,y 81 | 82 | # Bad variable names which should always be refused, separated by a comma 83 | bad-names=foo,bar,baz,toto,tutu,tata 84 | 85 | # List of builtins function names that should not be used, separated by a comma 86 | bad-functions=filter,apply,input 87 | 88 | [DESIGN] 89 | 90 | max-args=10 91 | max-locals=30 92 | max-returns=6 93 | max-branchs=18 94 | max-statements=50 95 | max-parents=5 96 | max-attributes=15 97 | min-public-methods=1 98 | max-public-methods=20 99 | 100 | [FORMAT] 101 | 102 | max-line-length=120 103 | max-module-lines=1000 104 | indent-string=' ' 105 | 106 | [SIMILARITIES] 107 | 108 | # Effectively disable similarity checking 109 | min-similarity-lines=10000 110 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is used by https://github.com/AzureAD/azure-activedirectory-library-for-python/blob/1.2.7/.travis.yml#L8 2 | 3 | requests>=2.25,<3 # request 2.25+ is the first version to allow urllib3 1.26.5+ thus bypass CVE-2021-33503 4 | PyJWT==2.4.0 5 | #need 2.x for Python3 support 6 | python-dateutil==2.1.0 7 | 8 | #1.1.0 is the first that can be installed on windows 9 | # Yet we decide to remove this from requirements.txt, 10 | # because ADAL does not have a direct dependency on it. 11 | #cryptography==3.2 12 | 13 | #for testing 14 | httpretty==0.8.14 15 | pylint==1.5.4 16 | -------------------------------------------------------------------------------- /sample/certificate_credentials_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import sys 5 | import adal 6 | 7 | def turn_on_logging(): 8 | logging.basicConfig(level=logging.DEBUG) 9 | #or, 10 | #handler = logging.StreamHandler() 11 | #adal.set_logging_options({ 12 | # 'level': 'DEBUG', 13 | # 'handler': handler 14 | #}) 15 | #handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT)) 16 | 17 | def get_private_key(filename): 18 | with open(filename, 'r') as pem_file: 19 | private_pem = pem_file.read() 20 | return private_pem 21 | 22 | # 23 | # You can provide account information by using a JSON file. Either 24 | # through a command line argument, 'python sample.py parameters.json', or 25 | # specifying in an environment variable of ADAL_SAMPLE_PARAMETERS_FILE. 26 | # privateKeyFile must contain a PEM encoded cert with private key. 27 | # thumbprint must be the thumbprint of the privateKeyFile. 28 | # 29 | # The information inside such file can be obtained via app registration. 30 | # See https://github.com/AzureAD/azure-activedirectory-library-for-python/wiki/Register-your-application-with-Azure-Active-Directory 31 | # 32 | # { 33 | # "resource": "your_resource", 34 | # "tenant" : "naturalcauses.onmicrosoft.com", 35 | # "authorityHostUrl" : "https://login.microsoftonline.com", 36 | # "clientId" : "d6835713-b745-48d1-bb62-7a8248477d35", 37 | # "thumbprint" : 'C15DEA8656ADDF67BE8031D85EBDDC5AD6C436E1', 38 | # "privateKeyFile" : 'ncwebCTKey.pem' 39 | # } 40 | parameters_file = (sys.argv[1] if len(sys.argv) == 2 else 41 | os.environ.get('ADAL_SAMPLE_PARAMETERS_FILE')) 42 | sample_parameters = {} 43 | if parameters_file: 44 | with open(parameters_file, 'r') as f: 45 | parameters = f.read() 46 | sample_parameters = json.loads(parameters) 47 | else: 48 | raise ValueError('Please provide parameter file with account information.') 49 | 50 | 51 | authority_url = (sample_parameters['authorityHostUrl'] + '/' + 52 | sample_parameters['tenant']) 53 | GRAPH_RESOURCE = '00000002-0000-0000-c000-000000000000' 54 | RESOURCE = sample_parameters.get('resource', GRAPH_RESOURCE) 55 | 56 | #uncomment for verbose logging 57 | turn_on_logging() 58 | 59 | ### Main logic begins 60 | context = adal.AuthenticationContext(authority_url) 61 | key = get_private_key(sample_parameters['privateKeyFile']) 62 | 63 | token = context.acquire_token_with_client_certificate( 64 | RESOURCE, 65 | sample_parameters['clientId'], 66 | key, 67 | sample_parameters['thumbprint']) 68 | ### Main logic ends 69 | 70 | print('Here is the token:') 71 | print(json.dumps(token, indent=2)) 72 | -------------------------------------------------------------------------------- /sample/client_credentials_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import sys 5 | import adal 6 | 7 | def turn_on_logging(): 8 | logging.basicConfig(level=logging.DEBUG) 9 | #or, 10 | #handler = logging.StreamHandler() 11 | #adal.set_logging_options({ 12 | # 'level': 'DEBUG', 13 | # 'handler': handler 14 | #}) 15 | #handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT)) 16 | 17 | # You can provide account information by using a JSON file. Either 18 | # through a command line argument, 'python sample.py parameters.json', or 19 | # specifying in an environment variable of ADAL_SAMPLE_PARAMETERS_FILE. 20 | # 21 | # The information inside such file can be obtained via app registration. 22 | # See https://github.com/AzureAD/azure-activedirectory-library-for-python/wiki/Register-your-application-with-Azure-Active-Directory 23 | # 24 | # { 25 | # "resource": "YOUR_RESOURCE", 26 | # "tenant" : "YOUR_SUB_DOMAIN.onmicrosoft.com", 27 | # "authorityHostUrl" : "https://login.microsoftonline.com", 28 | # "clientId" : "YOUR_CLIENTID", 29 | # "clientSecret" : "YOUR_CLIENTSECRET" 30 | # } 31 | 32 | 33 | parameters_file = (sys.argv[1] if len(sys.argv) == 2 else 34 | os.environ.get('ADAL_SAMPLE_PARAMETERS_FILE')) 35 | 36 | if parameters_file: 37 | with open(parameters_file, 'r') as f: 38 | parameters = f.read() 39 | sample_parameters = json.loads(parameters) 40 | else: 41 | raise ValueError('Please provide parameter file with account information.') 42 | 43 | authority_url = (sample_parameters['authorityHostUrl'] + '/' + 44 | sample_parameters['tenant']) 45 | GRAPH_RESOURCE = '00000002-0000-0000-c000-000000000000' 46 | RESOURCE = sample_parameters.get('resource', GRAPH_RESOURCE) 47 | 48 | #uncomment for verbose log 49 | #turn_on_logging() 50 | 51 | ### Main logic begins 52 | context = adal.AuthenticationContext( 53 | authority_url, validate_authority=sample_parameters['tenant'] != 'adfs', 54 | ) 55 | 56 | token = context.acquire_token_with_client_credentials( 57 | RESOURCE, 58 | sample_parameters['clientId'], 59 | sample_parameters['clientSecret']) 60 | ### Main logic ends 61 | 62 | print('Here is the token:') 63 | print(json.dumps(token, indent=2)) 64 | -------------------------------------------------------------------------------- /sample/device_code_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import sys 5 | import adal 6 | 7 | def turn_on_logging(): 8 | logging.basicConfig(level=logging.DEBUG) 9 | #or, 10 | #handler = logging.StreamHandler() 11 | #adal.set_logging_options({ 12 | # 'level': 'DEBUG', 13 | # 'handler': handler 14 | #}) 15 | #handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT)) 16 | 17 | # You can provide account information by using a JSON file 18 | # with the same parameters as the sampleParameters variable below. Either 19 | # through a command line argument, 'python sample.py parameters.json', or 20 | # specifying in an environment variable of ADAL_SAMPLE_PARAMETERS_FILE. 21 | # 22 | # The information inside such file can be obtained via app registration. 23 | # See https://github.com/AzureAD/azure-activedirectory-library-for-python/wiki/Register-your-application-with-Azure-Active-Directory 24 | # 25 | # { 26 | # "resource": "your_resource", 27 | # "tenant" : "rrandallaad1.onmicrosoft.com", 28 | # "authorityHostUrl" : "https://login.microsoftonline.com", 29 | # "clientId" : "624ac9bd-4c1c-4687-aec8-b56a8991cfb3", 30 | # "anothertenant" : "bar.onmicrosoft.com" 31 | # } 32 | 33 | parameters_file = (sys.argv[1] if len(sys.argv) == 2 else 34 | os.environ.get('ADAL_SAMPLE_PARAMETERS_FILE')) 35 | 36 | if parameters_file: 37 | with open(parameters_file, 'r') as f: 38 | parameters = f.read() 39 | sample_parameters = json.loads(parameters) 40 | else: 41 | raise ValueError('Please provide parameter file with account information.') 42 | 43 | 44 | authority_host_url = sample_parameters['authorityHostUrl'] 45 | authority_url = authority_host_url + '/' + sample_parameters['tenant'] 46 | clientid = sample_parameters['clientId'] 47 | GRAPH_RESOURCE = '00000002-0000-0000-c000-000000000000' 48 | RESOURCE = sample_parameters.get('resource', GRAPH_RESOURCE) 49 | 50 | #uncomment for verbose logging 51 | #turn_on_logging() 52 | 53 | ### Main logic begins 54 | context = adal.AuthenticationContext(authority_url) 55 | code = context.acquire_user_code(RESOURCE, clientid) 56 | print(code['message']) 57 | token = context.acquire_token_with_device_code(RESOURCE, code, clientid) 58 | ### Main logic ends 59 | 60 | print('Here is the token from "{}":'.format(authority_url)) 61 | print(json.dumps(token, indent=2)) 62 | 63 | #try cross tenant token refreshing 64 | another_tenant = sample_parameters.get('anothertenant') 65 | if another_tenant: 66 | authority_url = authority_host_url + '/' + another_tenant 67 | #reuse existing cache which has the tokens acquired early on 68 | existing_cache = context.cache 69 | context = adal.AuthenticationContext(authority_url, cache=existing_cache) 70 | token = context.acquire_token(RESOURCE, token['userId'], clientid) 71 | print('Here is the token from "{}":'.format(authority_url)) 72 | print(json.dumps(token, indent=2)) 73 | 74 | -------------------------------------------------------------------------------- /sample/refresh_token_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import sys 5 | import adal 6 | 7 | def turn_on_logging(): 8 | logging.basicConfig(level=logging.DEBUG) 9 | #or, 10 | #handler = logging.StreamHandler() 11 | #adal.set_logging_options({ 12 | # 'level': 'DEBUG', 13 | # 'handler': handler 14 | #}) 15 | #handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT)) 16 | 17 | # You can override the account information by using a JSON file. Either 18 | # through a command line argument, 'python sample.py parameters.json', or 19 | # specifying in an environment variable of ADAL_SAMPLE_PARAMETERS_FILE. 20 | # 21 | # The information inside such file can be obtained via app registration. 22 | # See https://github.com/AzureAD/azure-activedirectory-library-for-python/wiki/Register-your-application-with-Azure-Active-Directory 23 | # 24 | # { 25 | # "resource": "your_resource", 26 | # "tenant" : "rrandallaad1.onmicrosoft.com", 27 | # "authorityHostUrl" : "https://login.microsoftonline.com", 28 | # "clientId" : "624ac9bd-4c1c-4687-aec8-b56a8991cfb3", 29 | # "username" : "user1", 30 | # "password" : "verySecurePassword" 31 | # } 32 | 33 | parameters_file = (sys.argv[1] if len(sys.argv) == 2 else 34 | os.environ.get('ADAL_SAMPLE_PARAMETERS_FILE')) 35 | 36 | if parameters_file: 37 | with open(parameters_file, 'r') as f: 38 | parameters = f.read() 39 | sample_parameters = json.loads(parameters) 40 | else: 41 | raise ValueError('Please provide parameter file with account information.') 42 | 43 | authority_url = (sample_parameters['authorityHostUrl'] + '/' + 44 | sample_parameters['tenant']) 45 | GRAPH_RESOURCE = '00000002-0000-0000-c000-000000000000' 46 | RESOURCE = sample_parameters.get('resource', GRAPH_RESOURCE) 47 | 48 | #uncomment for verbose log 49 | #turn_on_logging() 50 | 51 | ### Main logic begins 52 | context = adal.AuthenticationContext( 53 | authority_url, validate_authority=sample_parameters['tenant'] != 'adfs', 54 | ) 55 | 56 | token = context.acquire_token_with_username_password( 57 | RESOURCE, 58 | sample_parameters['username'], 59 | sample_parameters['password'], 60 | sample_parameters['clientId']) 61 | 62 | print('Here is the token') 63 | print(json.dumps(token, indent=2)) 64 | 65 | refresh_token = token['refreshToken'] 66 | token = context.acquire_token_with_refresh_token( 67 | refresh_token, 68 | sample_parameters['clientId'], 69 | RESOURCE, 70 | # client_secret="your_secret" # This is needed when using Confidential Client, 71 | # otherwise you will encounter an invalid_client error. 72 | ) 73 | ### Main logic ends 74 | 75 | print('Here is the token acquired from the refreshing token') 76 | print(json.dumps(token, indent=2)) 77 | -------------------------------------------------------------------------------- /sample/website_sample.py: -------------------------------------------------------------------------------- 1 | try: 2 | from http import server as httpserver 3 | from http import cookies as Cookie 4 | except ImportError: 5 | import SimpleHTTPServer as httpserver 6 | import Cookie as Cookie 7 | 8 | try: 9 | import socketserver 10 | except ImportError: 11 | import SocketServer as socketserver 12 | 13 | try: 14 | from urllib.parse import urlparse, parse_qs 15 | except ImportError: 16 | from urlparse import urlparse, parse_qs 17 | 18 | import json 19 | import os 20 | import random 21 | import string 22 | import sys 23 | 24 | from adal import AuthenticationContext 25 | 26 | # You can provide account information by using a JSON file. Either 27 | # through a command line argument, 'python sample.py parameters.json', or 28 | # specifying in an environment variable of ADAL_SAMPLE_PARAMETERS_FILE. 29 | # 30 | # The information inside such file can be obtained via app registration. 31 | # See https://github.com/AzureAD/azure-activedirectory-library-for-python/wiki/Register-your-application-with-Azure-Active-Directory 32 | # 33 | # { 34 | # "resource": "your_resource", 35 | # "tenant" : "rrandallaad1.onmicrosoft.com", 36 | # "authorityHostUrl" : "https://login.microsoftonline.com", 37 | # "clientId" : "624ac9bd-4c1c-4687-aec8-b56a8991cfb3", 38 | # "clientSecret" : "verySecret="" 39 | # } 40 | 41 | parameters_file = (sys.argv[1] if len(sys.argv) == 2 else 42 | os.environ.get('ADAL_SAMPLE_PARAMETERS_FILE')) 43 | 44 | if parameters_file: 45 | with open(parameters_file, 'r') as f: 46 | parameters = f.read() 47 | sample_parameters = json.loads(parameters) 48 | else: 49 | raise ValueError('Please provide parameter file with account information.') 50 | 51 | PORT = 8088 52 | TEMPLATE_AUTHZ_URL = ('https://login.microsoftonline.com/{}/oauth2/authorize?'+ 53 | 'response_type=code&client_id={}&redirect_uri={}&'+ 54 | 'state={}&resource={}') 55 | GRAPH_RESOURCE = '00000002-0000-0000-c000-000000000000' 56 | RESOURCE = sample_parameters.get('resource', GRAPH_RESOURCE) 57 | REDIRECT_URI = 'http://localhost:{}/getAToken'.format(PORT) 58 | 59 | authority_url = (sample_parameters['authorityHostUrl'] + '/' + 60 | sample_parameters['tenant']) 61 | 62 | class OAuth2RequestHandler(httpserver.SimpleHTTPRequestHandler): 63 | def do_GET(self): 64 | if self.path == '/': 65 | self.send_response(307) 66 | login_url = 'http://localhost:{}/login'.format(PORT) 67 | self.send_header('Location', login_url) 68 | self.end_headers() 69 | elif self.path == '/login': 70 | auth_state = (''.join(random.SystemRandom() 71 | .choice(string.ascii_uppercase + string.digits) 72 | for _ in range(48))) 73 | cookie = Cookie.SimpleCookie() 74 | cookie['auth_state'] = auth_state 75 | authorization_url = TEMPLATE_AUTHZ_URL.format( 76 | sample_parameters['tenant'], 77 | sample_parameters['clientId'], 78 | REDIRECT_URI, 79 | auth_state, 80 | RESOURCE) 81 | self.send_response(307) 82 | self.send_header('Set-Cookie', cookie.output(header='')) 83 | self.send_header('Location', authorization_url) 84 | self.end_headers() 85 | elif self.path.startswith('/getAToken'): 86 | is_ok = True 87 | try: 88 | token_response = self._acquire_token() 89 | message = 'response: ' + json.dumps(token_response) 90 | #Later, if the access token is expired it can be refreshed. 91 | auth_context = AuthenticationContext(authority_url) 92 | token_response = auth_context.acquire_token_with_refresh_token( 93 | token_response['refreshToken'], 94 | sample_parameters['clientId'], 95 | RESOURCE, 96 | sample_parameters['clientSecret']) 97 | message = (message + '*** And here is the refresh response:' + 98 | json.dumps(token_response)) 99 | except ValueError as exp: 100 | message = str(exp) 101 | is_ok = False 102 | self._send_response(message, is_ok) 103 | 104 | def _acquire_token(self): 105 | parsed = urlparse(self.path) 106 | code = parse_qs(parsed.query)['code'][0] 107 | state = parse_qs(parsed.query)['state'][0] 108 | cookie = Cookie.SimpleCookie(self.headers["Cookie"]) 109 | if state != cookie['auth_state'].value: 110 | raise ValueError('state does not match') 111 | ### Main logic begins 112 | auth_context = AuthenticationContext(authority_url) 113 | return auth_context.acquire_token_with_authorization_code( 114 | code, 115 | REDIRECT_URI, 116 | RESOURCE, 117 | sample_parameters['clientId'], 118 | sample_parameters['clientSecret']) 119 | ### Main logic ends 120 | 121 | def _send_response(self, message, is_ok=True): 122 | self.send_response(200 if is_ok else 400) 123 | self.send_header('Content-type', 'text/html') 124 | self.end_headers() 125 | 126 | if is_ok: 127 | #todo, pretty format token response in json 128 | message_template = ('Succeeded' 129 | '

{}

') 130 | else: 131 | message_template = ('Failed' 132 | '

{}

') 133 | 134 | output = message_template.format(message) 135 | self.wfile.write(output.encode()) 136 | 137 | httpd = socketserver.TCPServer(('', PORT), OAuth2RequestHandler) 138 | 139 | print('serving at port', PORT) 140 | httpd.serve_forever() 141 | 142 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [metadata] 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #------------------------------------------------------------------------------ 3 | # 4 | # Copyright (c) Microsoft Corporation. 5 | # All rights reserved. 6 | # 7 | # This code is licensed under the MIT License. 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files(the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions : 15 | # 16 | # The above copyright notice and this permission notice shall be included in 17 | # all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | # THE SOFTWARE. 26 | # 27 | #------------------------------------------------------------------------------ 28 | 29 | from setuptools import setup 30 | import re, io 31 | 32 | # setup.py shall not import adal 33 | __version__ = re.search( 34 | r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', # It excludes inline comment too 35 | io.open('adal/__init__.py', encoding='utf_8_sig').read() 36 | ).group(1) 37 | 38 | # To build: 39 | # python setup.py sdist 40 | # python setup.py bdist_wheel 41 | # 42 | # To install: 43 | # python setup.py install 44 | # 45 | # To register (only needed once): 46 | # python setup.py register 47 | # 48 | # To upload: 49 | # python setup.py sdist upload 50 | # python setup.py bdist_wheel upload 51 | 52 | setup( 53 | name='adal', 54 | version=__version__, 55 | description=('Note: This library is already replaced by MSAL Python, ' + 56 | 'available here: https://pypi.org/project/msal/ .' + 57 | 'ADAL Python remains available here as a legacy. ' + 58 | 'The ADAL for Python library makes it easy for python ' + 59 | 'application to authenticate to Azure Active Directory ' + 60 | '(AAD) in order to access AAD protected web resources.'), 61 | license='MIT', 62 | author='Microsoft Corporation', 63 | author_email='nugetaad@microsoft.com', 64 | url='https://github.com/AzureAD/azure-activedirectory-library-for-python', 65 | classifiers=[ 66 | 'Development Status :: 7 - Inactive', 67 | 'Programming Language :: Python', 68 | 'Programming Language :: Python :: 2', 69 | 'Programming Language :: Python :: 2.7', 70 | 'Programming Language :: Python :: 3', 71 | 'Programming Language :: Python :: 3.3', 72 | 'Programming Language :: Python :: 3.4', 73 | 'Programming Language :: Python :: 3.5', 74 | 'Programming Language :: Python :: 3.6', 75 | 'License :: OSI Approved :: MIT License', 76 | ], 77 | packages=['adal'], 78 | install_requires=[ 79 | 'PyJWT>=1.0.0,<3', # ADAL does not use jwt.decode(), therefore is insusceptible to CVE-2022-29217 so no need to bump to PyJWT 2.4+ 80 | 'requests>=2.0.0,<3', 81 | 'python-dateutil>=2.1.0,<3', 82 | 'cryptography>=1.1.0' 83 | ] 84 | ) 85 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import sys 29 | import os 30 | from adal import authentication_parameters 31 | 32 | if sys.version_info[:2] < (2, 7, ): 33 | try: 34 | from unittest2 import TestLoader, TextTestRunner 35 | except ImportError: 36 | raise ImportError("The ADAL test suite requires the unittest2 " 37 | "package to run on Python 2.6 and below.\n" 38 | "Please install this package to continue.") 39 | else: 40 | from unittest import TestLoader, TextTestRunner 41 | 42 | if sys.version_info[:2] >= (3, 3, ): 43 | from unittest import mock 44 | else: 45 | try: 46 | import mock 47 | 48 | except ImportError: 49 | raise ImportError("The ADAL test suite requires the mock " 50 | "package to run on Python 3.2 and below.\n" 51 | "Please install this package to continue.") 52 | 53 | 54 | if __name__ == '__main__': 55 | 56 | runner = TextTestRunner(verbosity=2) 57 | 58 | test_dir = os.path.dirname(__file__) 59 | top_dir = os.path.dirname(os.path.dirname(test_dir)) 60 | test_loader = TestLoader() 61 | suite = test_loader.discover(test_dir, 62 | pattern="test_*.py", 63 | top_level_dir=top_dir) 64 | runner.run(suite) 65 | -------------------------------------------------------------------------------- /tests/config_sample.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | ''' 29 | This is a sample config file. Make a copy of it as config.py. Then follow the provided 30 | instructions to fill in your values. 31 | ''' 32 | ACQUIRE_TOKEN_WITH_USERNAME_PASSWORD = { 33 | # Getting token with username and passwords is the simple method. You need to create an Azure 34 | # Active Directory and a user. Once you have done this, you can put the tenant name, username 35 | # and password combination here. 36 | # 37 | # Note: You need to attempt to login to the user at least once to create a non-temp password. 38 | # To do this, go to http://manage.azure.com, sign in, create a new password, and use 39 | # the password created here. 40 | 41 | "username" : "USERNAME@XXXXXXXX.onmicrosoft.com", 42 | "password" : "None", 43 | "tenant" : "XXXXXXXX.onmicrosoft.com", 44 | 45 | "authorityHostUrl" : "https://login.microsoftonline.com", 46 | } 47 | 48 | ACQUIRE_TOKEN_WITH_CLIENT_CREDENTIALS = { 49 | # To use client credentials (Secret Key) you need to: 50 | # Create an Azure Active Directory (AD) instance on your Azure account 51 | # in this AD instance, create an application. I call mine PythonSDK http://PythonSDK 52 | # Go to the configure tab and you can find all of the following information: 53 | 54 | # Click on 'View Endpoints' and Copy the 'Federation Metadata Document' entry. 55 | # The root + GUID URL is our authority. 56 | "authority" : "https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL", 57 | 58 | # Exit out of App Endpoints. The client Id is on the configure page. 59 | "client_id" : "ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL", 60 | 61 | # In the keys section of the Azure AD App Configure page, create a key (1 or 2 years is fine) 62 | "secret" : "a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a=", 63 | 64 | # NOTE: If this is to be used with ARM (the case of the Azure SDK) you will need to grant 65 | # permissions to that service. At this time Azure does not have this in the portal for 66 | # Azure Resource Management. 67 | # Here is an example using POSH (Powershell) Tools for Azure to grant those rights. 68 | # Switch-AzureMode -Name AzureResourceManager 69 | # Add-AzureAccount # This will pop up a login dialog 70 | # # Look at the subscriptions returned and put one on the line below 71 | # Select-AzureSubscription -SubscriptionId ABCDEFGH-1234-1234-1234-ABCDEFGH 72 | # New-AzureRoleAssignment -ServicePrincipalName http://PythonSDK -RoleDefinitionName Contributor 73 | } 74 | 75 | 76 | # TODO: ADD DICTIONARIES FOR THE OTHER TESTS 77 | # ACQUIRE_TOKEN_WITH_AUTHORIZATION_CODE 78 | # ACQUIRE_TOKEN_WITH_REFRESH_TOKEN 79 | # ACQUIRE_TOKEN_WITH_CLIENT_CERTIFICATE 80 | -------------------------------------------------------------------------------- /tests/test_api_version.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import warnings 29 | try: 30 | import unittest2 as unittest 31 | except ImportError: 32 | import unittest 33 | 34 | try: 35 | from unittest import mock 36 | except ImportError: 37 | import mock 38 | 39 | import adal 40 | 41 | class TestAuthenticationContextApiVersionBehavior(unittest.TestCase): 42 | 43 | def test_api_version_default_value(self): 44 | with warnings.catch_warnings(record=True) as caught_warnings: 45 | warnings.simplefilter("always") 46 | context = adal.AuthenticationContext( 47 | "https://login.microsoftonline.com/tenant") 48 | self.assertEqual(context._call_context['api_version'], None) 49 | self.assertEqual(len(caught_warnings), 0) 50 | if len(caught_warnings) == 1: 51 | # It should be len(caught_warnings)==1, but somehow it works on 52 | # all my local test environment but not on Travis-CI. 53 | # So we relax this check, for now. 54 | self.assertIn("deprecated", str(caught_warnings[0].message)) 55 | 56 | def test_explicitly_turn_off_api_version(self): 57 | with warnings.catch_warnings(record=True) as caught_warnings: 58 | warnings.simplefilter("always") 59 | context = adal.AuthenticationContext( 60 | "https://login.microsoftonline.com/tenant", api_version=None) 61 | self.assertEqual(context._call_context['api_version'], None) 62 | self.assertEqual(len(caught_warnings), 0) 63 | 64 | class TestOAuth2ClientApiVersionBehavior(unittest.TestCase): 65 | 66 | authority = mock.Mock(token_endpoint="https://example.com/token") 67 | 68 | def test_api_version_is_set(self): 69 | client = adal.oauth2_client.OAuth2Client( 70 | {"api_version": "1.0", "log_context": mock.Mock()}, self.authority) 71 | self.assertIn('api-version=1.0', client._create_token_url().geturl()) 72 | 73 | def test_api_version_is_not_set(self): 74 | client = adal.oauth2_client.OAuth2Client( 75 | {"api_version": None, "log_context": mock.Mock()}, self.authority) 76 | self.assertNotIn('api-version=1.0', client._create_token_url().geturl()) 77 | 78 | if __name__ == '__main__': 79 | unittest.main() 80 | 81 | -------------------------------------------------------------------------------- /tests/test_authorization_code.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import sys 29 | import requests 30 | import httpretty 31 | 32 | try: 33 | import unittest2 as unittest 34 | except ImportError: 35 | import unittest 36 | 37 | try: 38 | from unittest import mock 39 | except ImportError: 40 | import mock 41 | 42 | import adal 43 | from adal.authentication_context import AuthenticationContext 44 | from tests import util 45 | from tests.util import parameters as cp 46 | 47 | try: 48 | from urllib.parse import urlparse, urlencode 49 | except ImportError: 50 | from urllib import urlencode 51 | from urlparse import urlparse 52 | 53 | 54 | class TestAuthorizationCode(unittest.TestCase): 55 | 56 | def setup_expected_auth_code_token_request_response(self, httpCode, returnDoc, authorityEndpoint=None): 57 | if authorityEndpoint is None: 58 | authorityEndpoint = '{}{}?slice=testslice&api-version=1.0'.format(cp['authUrl'], cp['tokenPath']) 59 | 60 | queryParameters = {} 61 | queryParameters['grant_type'] = 'authorization_code' 62 | queryParameters['code'] = self.authorization_code 63 | queryParameters['client_id'] = cp['clientId'] 64 | queryParameters['client_secret'] = cp['clientSecret'] 65 | queryParameters['resource'] = cp['resource'] 66 | queryParameters['redirect_uri'] = self.redirect_uri 67 | 68 | query = urlencode(queryParameters) 69 | 70 | def func(body): 71 | return util.filter_query_strings(query, body) 72 | 73 | import json 74 | returnDocJson = json.dumps(returnDoc) 75 | httpretty.register_uri(httpretty.POST, authorityEndpoint, returnDocJson, status = httpCode, content_type = 'text/json') 76 | 77 | def setUp(self): 78 | self.authorization_code = '1234870909' 79 | self.redirect_uri = 'app_bundle:foo.bar.baz' 80 | 81 | @httpretty.activate 82 | def test_happy_path(self): 83 | response = util.create_response() 84 | self.setup_expected_auth_code_token_request_response(200, response['wireResponse']) 85 | 86 | context = adal.AuthenticationContext(cp['authUrl']) 87 | 88 | # action 89 | token_response = context.acquire_token_with_authorization_code(self.authorization_code, self.redirect_uri, response['resource'], cp['clientId'], cp['clientSecret']) 90 | 91 | # assert 92 | 93 | # the caching layer adds a few extra fields, let us pop them out for easier verification 94 | for k in ['_clientId', '_authority', 'resource']: 95 | token_response.pop(k, None) 96 | self.assertTrue(util.is_match_token_response(response['decodedResponse'], token_response), 'The response did not match what was expected') 97 | 98 | # verify a request on the wire was made 99 | req = httpretty.last_request() 100 | util.match_standard_request_headers(req) 101 | 102 | # verify the same token entry was cached 103 | cached_items = list(context.cache.read_items()) 104 | self.assertTrue(len(cached_items) == 1) 105 | _, cached_entry = cached_items[0] 106 | self.assertEqual(cached_entry, token_response) 107 | 108 | def test_failed_http_request(self): 109 | with self.assertRaises(Exception): 110 | adal._acquire_token_with_authorization_code( 111 | 'https://0.1.1.1:12/my.tenant.com', cp['clientId'], cp['clientSecret'], 112 | self.authorization_code, self.redirect_uri, response['resource']) 113 | 114 | def test_bad_argument(self): 115 | with self.assertRaises(Exception): 116 | adal._acquire_token_with_authorization_code( 117 | 'https://0.1.1.1:12/my.tenant.com', cp['clientId'], cp['clientSecret'], 118 | self.authorization_code, self.redirect_uri, 'BogusResource') 119 | if __name__ == '__main__': 120 | unittest.main() 121 | -------------------------------------------------------------------------------- /tests/test_cache_driver.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import unittest 29 | try: 30 | from unittest import mock 31 | except ImportError: 32 | import mock 33 | 34 | from adal.log import create_log_context 35 | from adal.cache_driver import CacheDriver 36 | 37 | 38 | class TestCacheDriver(unittest.TestCase): 39 | def test_rt_less_item_wont_cause_exception(self): # Github issue #82 40 | rt_less_entry_came_from_previous_client_credentials_grant = { 41 | "expiresIn": 3600, 42 | "_authority": "https://login.microsoftonline.com/foo", 43 | "resource": "spn:00000002-0000-0000-c000-000000000000", 44 | "tokenType": "Bearer", 45 | "expiresOn": "1999-05-22 16:31:46.202000", 46 | "isMRRT": True, 47 | "_clientId": "client_id", 48 | "accessToken": "this is an AT", 49 | } 50 | refresh_function = mock.MagicMock(return_value={}) 51 | cache_driver = CacheDriver( 52 | {"log_context": create_log_context()}, "authority", "resource", 53 | "client_id", mock.MagicMock(), refresh_function) 54 | entry = cache_driver._refresh_entry_if_necessary( 55 | rt_less_entry_came_from_previous_client_credentials_grant, False) 56 | refresh_function.assert_not_called() # Otherwise it will cause an exception 57 | self.assertIsNone(entry) 58 | 59 | -------------------------------------------------------------------------------- /tests/test_client_credentials.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import unittest 29 | import json 30 | import httpretty 31 | import six 32 | 33 | import adal 34 | from adal.self_signed_jwt import SelfSignedJwt 35 | from adal.authentication_context import AuthenticationContext 36 | from tests import util 37 | from tests.util import parameters as cp 38 | 39 | class TestClientCredentials(unittest.TestCase): 40 | def setUp(self): 41 | util.reset_logging() 42 | util.clear_static_cache() 43 | 44 | def tearDown(self): 45 | util.reset_logging() 46 | util.clear_static_cache() 47 | 48 | @httpretty.activate 49 | def test_happy_path(self): 50 | response_options = { 'noRefresh' : True, 'tokenEndpoint': True } 51 | response = util.create_response(response_options) 52 | token_request = util.setup_expected_client_cred_token_request_response(200, response['wireResponse']) 53 | 54 | context = adal.AuthenticationContext(cp['authUrl']) 55 | token_response = context.acquire_token_with_client_credentials( 56 | response['resource'], cp['clientId'], cp['clientSecret']) 57 | self.assertTrue( 58 | util.is_match_token_response(response['cachedResponse'], token_response), 59 | 'The response does not match what was expected.: ' + str(token_response) 60 | ) 61 | 62 | @httpretty.activate 63 | def test_http_error(self): 64 | tokenRequest = util.setup_expected_client_cred_token_request_response(403) 65 | 66 | with six.assertRaisesRegex(self, Exception, '403'): 67 | context = adal.AuthenticationContext(cp['authUrl']) 68 | token_response = context.acquire_token_with_client_credentials( 69 | cp['resource'], cp['clientId'], cp['clientSecret']) 70 | 71 | @httpretty.activate 72 | def test_oauth_error(self): 73 | errorResponse = { 74 | 'error' : 'invalid_client', 75 | 'error_description' : 'This is a test error description', 76 | 'error_uri' : 'http://errordescription.com/invalid_client.html' 77 | } 78 | 79 | tokenRequest = util.setup_expected_client_cred_token_request_response(400, errorResponse) 80 | 81 | with six.assertRaisesRegex(self, Exception, 'Get Token request returned http error: 400 and server response:'): 82 | context = adal.AuthenticationContext(cp['authUrl']) 83 | token_response = context.acquire_token_with_client_credentials( 84 | cp['resource'], cp['clientId'], cp['clientSecret']) 85 | 86 | @httpretty.activate 87 | def test_error_with_junk_return(self): 88 | junkResponse = 'This is not properly formatted return value.' 89 | 90 | tokenRequest = util.setup_expected_client_cred_token_request_response(400, junkResponse) 91 | 92 | with self.assertRaises(Exception): 93 | context = adal.AuthenticationContext(cp['authUrl']) 94 | token_response = context.acquire_token_with_client_credentials( 95 | cp['resource'], cp['clientId'], cp['clientSecret']) 96 | 97 | @httpretty.activate 98 | def test_success_with_junk_return(self): 99 | junkResponse = 'This is not properly formatted return value.' 100 | 101 | tokenRequest = util.setup_expected_client_cred_token_request_response(200, junkResponse) 102 | 103 | with self.assertRaises(Exception): 104 | context = adal.AuthenticationContext(cp['authUrl']) 105 | token_response = context.acquire_token_with_client_credentials( 106 | cp['resource'], cp['clientId'], cp['clientSecret']) 107 | 108 | def test_no_cached_token_found_error(self): 109 | context = AuthenticationContext(cp['authUrl']) 110 | 111 | try: 112 | context.acquire_token(cp['resource'], 'unknownUser', cp['clientId']) 113 | except Exception as err: 114 | self.assertTrue(err, 'Expected an error and non was received.') 115 | self.assertIn('not found', err.args[0], 'Returned error did not contain expected message: ' + err.args[0]) 116 | 117 | 118 | def update_self_signed_jwt_stubs(): 119 | ''' 120 | function updateSelfSignedJwtStubs() { 121 | savedProto = {} 122 | savedProto._getDateNow = SelfSignedJwt._getDateNow 123 | savedProto._getNewJwtId = SelfSignedJwt._getNewJwtId 124 | 125 | SelfSignedJwt.prototype._getDateNow = function() { return cp['nowDate'] } 126 | SelfSignedJwt.prototype._getNewJwtId = function() { return cp['jwtId'] } 127 | 128 | return savedProto 129 | } 130 | ''' 131 | raise NotImplementedError() 132 | 133 | def reset_self_signed_jwt_stubs(safe_proto): 134 | ''' 135 | function resetSelfSignedJwtStubs(saveProto) { 136 | _.extend(SelfSignedJwt, saveProto) 137 | } 138 | ''' 139 | raise NotImplementedError() 140 | 141 | @unittest.skip('https://github.com/AzureAD/azure-activedirectory-library-for-python-priv/issues/20') 142 | # TODO TODO: setupExpectedClientAssertionTokenRequestResponse, updateSelfSignedJwtStubs 143 | @httpretty.activate 144 | def test_cert_happy_path(self): 145 | ''' TODO: Test Failing as of 2015/06/03 and needs to be completed. ''' 146 | self.fail("Not Yet Implemented. Add Helper Functions and setup method") 147 | saveProto = updateSelfSignedJwtStubs() 148 | 149 | responseOptions = { noRefresh : true } 150 | response = util.create_response(responseOptions) 151 | tokenRequest = util.setupExpectedClientAssertionTokenRequestResponse(200, response.wireResponse, cp['authorityTenant']) 152 | context = adal.AuthenticationContext(cp['authorityTenant']) 153 | 154 | context.acquire_token_with_client_certificate(response.resource, cp['clientId'], cp['cert'], cp['certHash']) 155 | 156 | resetSelfSignedJwtStubs(saveProto) 157 | self.assertTrue(util.is_match_token_response(response.cachedResponse, token_response), 'The response did not match what was expected') 158 | 159 | def test_cert_bad_cert(self): 160 | cert = 'gobbledy' 161 | context = adal.AuthenticationContext(cp['authorityTenant']) 162 | 163 | with six.assertRaisesRegex(self, Exception, "Error:Invalid Certificate: Expected Start of Certificate to be '-----BEGIN RSA PRIVATE KEY-----'"): 164 | context.acquire_token_with_client_certificate(cp['resource'], cp['clientId'], cert, cp['certHash']) 165 | 166 | def test_cert_bad_thumbprint(self): 167 | thumbprint = 'gobbledy' 168 | context = adal.AuthenticationContext(cp['authorityTenant']) 169 | 170 | with six.assertRaisesRegex(self, Exception, 'thumbprint does not match a known format'): 171 | context.acquire_token_with_client_certificate( cp['resource'], cp['clientId'], cp['cert'], thumbprint) 172 | 173 | 174 | if __name__ == '__main__': 175 | unittest.main() 176 | -------------------------------------------------------------------------------- /tests/test_e2e_examples.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import unittest 29 | import base64 30 | import json 31 | import adal 32 | 33 | try: 34 | from tests.config import ACQUIRE_TOKEN_WITH_USERNAME_PASSWORD as user_pass_params 35 | from tests.config import ACQUIRE_TOKEN_WITH_CLIENT_CREDENTIALS as client_cred_params 36 | 37 | #per http://stackoverflow.com/questions/12487532/how-do-i-skip-a-whole-python-unittest-module-at-run-time 38 | 39 | class TestE2EExamples(unittest.TestCase): 40 | 41 | def setUp(self): 42 | self.assertIsNotNone(user_pass_params['password'], "This test cannot work without you adding a password") 43 | return super(TestE2EExamples, self).setUp() 44 | 45 | def test_acquire_token_with_user_pass_explicit(self): 46 | resource = '00000002-0000-0000-c000-000000000000' 47 | client_id_xplat = '04b07795-8ddb-461a-bbee-02f9e1bf7b46' 48 | authority = user_pass_params['authorityHostUrl'] + '/' + user_pass_params['tenant'] 49 | 50 | context = adal.AuthenticationContext(authority) 51 | token_response = context.acquire_token_with_username_password( 52 | resource, user_pass_params['username'], user_pass_params['password'], 53 | client_id_xplat) 54 | self.validate_token_response_username_password(token_response) 55 | 56 | def test_acquire_token_with_client_creds(self): 57 | resource = '00000002-0000-0000-c000-000000000000' 58 | context = adal.AuthenticationContext(client_cred_params['authority']) 59 | token_response = context.acquire_token_with_client_credentials( 60 | resource, 61 | client_cred_params['clientId'], 62 | client_cred_params['secret']) 63 | 64 | self.validate_token_response_client_credentials(token_response) 65 | 66 | @unittest.skip('https://github.com/AzureAD/azure-activedirectory-library-for-python-priv/issues/46') 67 | def test_acquire_token_with_authorization_code(self): 68 | self.fail("Not Yet Implemented") 69 | 70 | def test_acquire_token_with_refresh_token(self): 71 | authority = user_pass_params['authorityHostUrl'] + '/' + user_pass_params['tenant'] 72 | resource = '00000002-0000-0000-c000-000000000000' 73 | client_id_xplat = '04b07795-8ddb-461a-bbee-02f9e1bf7b46' 74 | 75 | # Get token using username password first 76 | context = adal.AuthenticationContext(authority) 77 | token_response = context.acquire_token_with_username_password( 78 | resource, user_pass_params['username'], user_pass_params['password'], 79 | client_id_xplat) 80 | self.validate_token_response_username_password(token_response) 81 | 82 | # Use returned refresh token to acquire a new token. 83 | refresh_token = token_response['refreshToken'] 84 | context = adal.AuthenticationContext(authority) 85 | token_response2 = context.acquire_token_with_refresh_token(refresh_token, client_id_xplat, resource) 86 | self.validate_token_response_refresh_token(token_response2) 87 | 88 | @unittest.skip('https://github.com/AzureAD/azure-activedirectory-library-for-python-priv/issues/47') 89 | def test_acquire_token_with_client_certificate(self): 90 | self.fail("Not Yet Implemented") 91 | 92 | 93 | # Validation Methods 94 | def validate_keys_in_dict(self, dict, keys): 95 | for i in keys: 96 | self.assertIn(i, dict) 97 | 98 | def validate_token_response_username_password(self, token_response): 99 | self.validate_keys_in_dict( 100 | token_response, 101 | [ 102 | 'accessToken', 'expiresIn', 'expiresOn', 'familyName', 'givenName', 103 | 'refreshToken', 'resource', 'tenantId', 'tokenType', 104 | ] 105 | ) 106 | 107 | def validate_token_response_client_credentials(self, token_response): 108 | self.validate_keys_in_dict( 109 | token_response, 110 | ['accessToken', 'expiresIn', 'expiresOn', 'resource', 'tokenType'] 111 | ) 112 | 113 | @unittest.skip('https://github.com/AzureAD/azure-activedirectory-library-for-python-priv/issues/46') 114 | def validate_token_response_authorization_code(self, token_response): 115 | self.fail("Not Yet Implemented") 116 | 117 | def validate_token_response_refresh_token(self, token_response): 118 | self.validate_keys_in_dict( 119 | token_response, 120 | [ 121 | 'accessToken', 'expiresIn', 'expiresOn', 'refreshToken', 'resource', 'tokenType' 122 | ] 123 | ) 124 | 125 | @unittest.skip('https://github.com/AzureAD/azure-activedirectory-library-for-python-priv/issues/47') 126 | def validate_token_response_client_certificate(self, token_response): 127 | self.fail("Not Yet Implemented") 128 | 129 | except: 130 | print ("WARNING: E2E example testing were skipped, for missing 'config.py'.") 131 | 132 | 133 | if __name__ == '__main__': 134 | unittest.main() 135 | -------------------------------------------------------------------------------- /tests/test_log.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | import json 28 | import logging 29 | import unittest 30 | try: 31 | from cStringIO import StringIO 32 | except ImportError: 33 | from io import StringIO 34 | 35 | from adal import log as adal_logging 36 | from tests import util 37 | from tests.util import parameters as cp 38 | 39 | class TestLog(unittest.TestCase): 40 | def test_settings_none(self): 41 | current_options = adal_logging.get_logging_options() 42 | 43 | adal_logging.set_logging_options() 44 | 45 | options = adal_logging.get_logging_options() 46 | adal_logging.set_logging_options(current_options) 47 | 48 | noOptions = len(options) == 1 and options['level'] == 'ERROR' 49 | self.assertTrue(noOptions, 'Did not expect to find any logging options set: ' + json.dumps(options)) 50 | 51 | def test_console_settings(self): 52 | currentOptions = adal_logging.get_logging_options() 53 | util.turn_on_logging() 54 | options = adal_logging.get_logging_options() 55 | level = options['level'] 56 | 57 | # Set the looging options back to what they were before this test so that 58 | # future tests are logged as they should be. 59 | adal_logging.set_logging_options(currentOptions) 60 | 61 | self.assertEqual(level, 'DEBUG', 'Logging level was not the expected value of LOGGING_LEVEL.DEBUG: {}'.format(level)) 62 | 63 | def test_logging(self): 64 | log_capture_string = StringIO() 65 | handler = logging.StreamHandler(log_capture_string) 66 | util.turn_on_logging(handler=handler) 67 | 68 | test_logger = adal_logging.Logger("TokenRequest", {'correlation_id':'12345'}) 69 | test_logger.warn('a warning', log_stack_trace=True) 70 | log_contents = log_capture_string.getvalue() 71 | logging.getLogger(adal_logging.ADAL_LOGGER_NAME).removeHandler(handler) 72 | self.assertTrue('12345 - TokenRequest:a warning' in log_contents and 'Stack:' in log_contents) 73 | 74 | def test_scrub_pii(self): 75 | not_pii = "not pii" 76 | pii = "pii@contoso.com" 77 | content_with_pii = {"message": not_pii, "email": pii} 78 | expected = {"message": not_pii, "email": "..."} 79 | self.assertEqual(adal_logging.scrub_pii(content_with_pii), expected) 80 | 81 | log_capture_string = StringIO() 82 | handler = logging.StreamHandler(log_capture_string) 83 | util.turn_on_logging(handler=handler) 84 | test_logger = adal_logging.Logger("TokenRequest", {'correlation_id':'12345'}) 85 | test_logger.warn('%(message)s for user email %(email)s', content_with_pii) 86 | log_contents = log_capture_string.getvalue() 87 | logging.getLogger(adal_logging.ADAL_LOGGER_NAME).removeHandler(handler) 88 | self.assertTrue(not_pii in log_contents and pii not in log_contents) 89 | 90 | 91 | if __name__ == '__main__': 92 | unittest.main() 93 | -------------------------------------------------------------------------------- /tests/test_mex.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | import os 28 | import unittest 29 | import httpretty 30 | from tests import util 31 | from adal.mex import Mex 32 | 33 | cp = util.parameters 34 | 35 | class Test_Mex(unittest.TestCase): 36 | def test_happy_path_1(self): 37 | self._happyPathTest('microsoft.mex.xml', 'https://corp.sts.microsoft.com/adfs/services/trust/13/usernamemixed') 38 | 39 | def test_happy_path_2(self): 40 | self._happyPathTest('arupela.mex.xml', 'https://fs.arupela.com/adfs/services/trust/13/usernamemixed') 41 | 42 | def test_happy_path_3(self): 43 | self._happyPathTest('archan.us.mex.xml', 'https://arvmserver2012.archan.us/adfs/services/trust/13/usernamemixed') 44 | 45 | @httpretty.activate 46 | def test_failed_request(self): 47 | httpretty.register_uri(httpretty.GET, cp['adfsMex'], status = 500) 48 | 49 | mex = Mex(cp['callContext'], cp['adfsMex']) 50 | 51 | try: 52 | mex.discover() 53 | self.fail('No exception was thrown caused by failed request') 54 | except Exception as exp: 55 | self.assertEqual(exp.args[0], 'Mex Get request returned http error: 500 and server response: HTTPretty :)') 56 | 57 | @httpretty.activate 58 | def _happyPathTest(self, file_name, expectedUrl): 59 | mexDocPath = os.path.join(os.getcwd(), 'tests', 'mex', file_name) 60 | mexFile = open(mexDocPath) 61 | mexDoc = mexFile.read() 62 | mexFile.close() 63 | httpretty.register_uri(httpretty.GET, cp['adfsMex'], body = mexDoc, status = 200) 64 | 65 | mex = Mex(cp['callContext'], cp['adfsMex']) 66 | mex.discover() 67 | url = mex.username_password_policy['url'] 68 | self.assertEqual(url, expectedUrl, 'returned url did not match: {}:{}'.format(expectedUrl, url)) 69 | 70 | @httpretty.activate 71 | def _badMexDocTest(self, file_name): 72 | mexDocPath = os.path.join(os.getcwd(), 'tests', 'mex', file_name) 73 | mexFile = open(mexDocPath) 74 | mexDoc = mexFile.read() 75 | mexFile.close() 76 | httpretty.register_uri(httpretty.GET, cp['adfsMex'], body = mexDoc, status = 200) 77 | 78 | mex = Mex(cp['callContext'], cp['adfsMex']) 79 | 80 | with self.assertRaises(Exception): 81 | mex.discover() 82 | 83 | if __name__ == '__main__': 84 | unittest.main() 85 | -------------------------------------------------------------------------------- /tests/test_refresh_token.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import sys 29 | import requests 30 | import httpretty 31 | import json 32 | 33 | try: 34 | import unittest2 as unittest 35 | except ImportError: 36 | import unittest 37 | 38 | try: 39 | from unittest import mock 40 | except ImportError: 41 | import mock 42 | 43 | import adal 44 | from adal.authentication_context import AuthenticationContext, TokenCache 45 | from tests import util 46 | from tests.util import parameters as cp 47 | 48 | class TestRefreshToken(unittest.TestCase): 49 | 50 | @httpretty.activate 51 | def test_happy_path_with_resource_client_secret(self): 52 | response_options = { 'refreshedRefresh' : True } 53 | response = util.create_response(response_options) 54 | wire_response = response['wireResponse'] 55 | tokenRequest = util.setup_expected_refresh_token_request_response(200, wire_response, response['authority'], response['resource'], cp['clientSecret']) 56 | 57 | context = adal.AuthenticationContext(cp['authorityTenant']) 58 | def side_effect (tokenfunc): 59 | return response['decodedResponse'] 60 | 61 | context._acquire_token = mock.MagicMock(side_effect=side_effect) 62 | 63 | token_response = context.acquire_token_with_refresh_token(cp['refreshToken'], cp['clientId'], cp['clientSecret'], cp['resource']) 64 | self.assertTrue( 65 | util.is_match_token_response(response['decodedResponse'], token_response), 66 | 'The response did not match what was expected: ' + str(token_response) 67 | ) 68 | 69 | @httpretty.activate 70 | def test_happy_path_with_resource_adfs(self): 71 | # arrange 72 | # set up token refresh result 73 | wire_response = util.create_response({ 74 | 'refreshedRefresh' : True, 75 | 'mrrt': False 76 | })['wireResponse'] 77 | new_resource = 'https://graph.local.azurestack.external/' 78 | tokenRequest = util.setup_expected_refresh_token_request_response(200, wire_response, cp['authority'], new_resource) 79 | 80 | # set up an existing token to be used for refreshing 81 | existing_token = util.create_response({ 82 | 'refreshedRefresh': True, 83 | 'mrrt': True 84 | })['decodedResponse'] 85 | existing_token['_clientId'] = existing_token.get('_clientId') or cp['clientId'] 86 | existing_token['isMRRT'] = existing_token.get('isMRRT') or True 87 | existing_token['_authority'] = existing_token.get('_authority') or cp['authorizeUrl'] 88 | token_cache = TokenCache(json.dumps([existing_token])) 89 | 90 | # act 91 | user_id = existing_token['userId'] 92 | context = adal.AuthenticationContext(cp['authorityTenant'], cache=token_cache) 93 | token_response = context.acquire_token(new_resource, user_id, cp['clientId']) 94 | 95 | # assert 96 | tokens = [value for key, value in token_cache.read_items()] 97 | self.assertEqual(2, len(tokens)) 98 | self.assertEqual({cp['resource'], new_resource}, set([x['resource'] for x in tokens])) 99 | 100 | if __name__ == '__main__': 101 | unittest.main() 102 | -------------------------------------------------------------------------------- /tests/test_self_signed_jwt.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import sys 29 | import requests 30 | import httpretty 31 | import json 32 | from datetime import datetime 33 | 34 | try: 35 | import unittest2 as unittest 36 | except ImportError: 37 | import unittest 38 | 39 | try: 40 | from unittest import mock 41 | except ImportError: 42 | import mock 43 | 44 | import adal 45 | from adal.authority import Authority 46 | from adal import self_signed_jwt 47 | from adal.self_signed_jwt import SelfSignedJwt 48 | from adal.authentication_context import AuthenticationContext 49 | from tests import util 50 | from tests.util import parameters as cp 51 | 52 | class TestSelfSignedJwt(unittest.TestCase): 53 | testNowDate = cp['nowDate'] 54 | testJwtId = cp['jwtId'] 55 | expectedJwtWithThumbprint = cp['expectedJwtWithThumbprint'] 56 | expectedJwtWithPublicCert = cp['expectedJwtWithPublicCert'] 57 | 58 | unexpectedJwt = 'unexpectedJwt' 59 | testAuthority = Authority('https://login.microsoftonline.com/naturalcauses.com', False) 60 | testClientId = 'd6835713-b745-48d1-bb62-7a8248477d35' 61 | testCert = cp['cert'] 62 | testPublicCert=cp['publicCert'] 63 | 64 | def _create_jwt(self, cert, thumbprint, public_certificate = None, encodeError = None): 65 | ssjwt = SelfSignedJwt(cp['callContext'], self.testAuthority, self.testClientId) 66 | 67 | self_signed_jwt._get_date_now = mock.MagicMock(return_value = self.testNowDate) 68 | self_signed_jwt._get_new_jwt_id = mock.MagicMock(return_value = self.testJwtId) 69 | 70 | if encodeError: 71 | self_signed_jwt._encode_jwt = mock.MagicMock(return_value = self.unexpectedJwt) 72 | else: 73 | expected = self.expectedJwtWithPublicCert if public_certificate else self.expectedJwtWithThumbprint 74 | self_signed_jwt._encode_jwt = mock.MagicMock(return_value = expected) 75 | 76 | jwt = ssjwt.create(cert, thumbprint, public_certificate=public_certificate) 77 | return jwt 78 | 79 | def _create_jwt_and_match_expected_err(self, testCert, thumbprint, encodeError = None): 80 | with self.assertRaises(Exception): 81 | self._create_jwt(testCert, thumbprint, encodeError = encodeError) 82 | 83 | def _create_jwt_and_match_expected_jwt(self, cert, thumbprint): 84 | jwt = self._create_jwt(cert, thumbprint) 85 | self.assertTrue(jwt, 'No JWT generated') 86 | self.assertTrue(jwt == self.expectedJwtWithThumbprint, 'Generated JWT does not match expected:{}'.format(jwt)) 87 | 88 | def test_jwt_hash_with_public_cert(self): 89 | jwt = self._create_jwt(self.testCert, cp['certHash'], public_certificate = self.testPublicCert) 90 | self.assertTrue(jwt == self.expectedJwtWithPublicCert, 'Generated JWT does not match expected:{}'.format(jwt)) 91 | 92 | def test_create_jwt_hash_colons(self): 93 | self._create_jwt_and_match_expected_jwt(self.testCert, cp['certHash']) 94 | 95 | def test_create_jwt_hash_spaces(self): 96 | thumbprint = cp['certHash'].replace(':', ' ') 97 | self._create_jwt_and_match_expected_jwt(self.testCert, thumbprint) 98 | 99 | def test_create_jwt_hash_straight_hex(self): 100 | thumbprint = cp['certHash'].replace(':', '') 101 | self._create_jwt_and_match_expected_jwt(self.testCert, thumbprint) 102 | 103 | def test_create_jwt_invalid_cert(self): 104 | self._create_jwt_and_match_expected_err('foobar', cp['certHash'], encodeError = True) 105 | 106 | def test_create_jwt_invalid_thumbprint_1(self): 107 | self._create_jwt_and_match_expected_err(self.testCert, 'zzzz') 108 | 109 | def test_create_jwt_invalid_thumbprint_wrong_size(self): 110 | thumbprint = 'C1:5D:EA:86:56:AD:DF:67:BE:80:31:D8:5E:BD:DC:5A:D6:C4:36:E7:AA' 111 | self._create_jwt_and_match_expected_err(self.testCert, thumbprint) 112 | 113 | def test_create_jwt_invalid_thumbprint_invalid_char(self): 114 | thumbprint = 'C1:5D:EA:86:56:AD:DF:67:BE:80:31:D8:5E:BD:DC:5A:D6:C4:36:Ez' 115 | self._create_jwt_and_match_expected_err(self.testCert, thumbprint) 116 | 117 | if __name__ == '__main__': 118 | unittest.main() 119 | -------------------------------------------------------------------------------- /tests/test_user_realm.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import sys 29 | import requests 30 | import httpretty 31 | 32 | try: 33 | import unittest2 as unittest 34 | except ImportError: 35 | import unittest 36 | 37 | try: 38 | from unittest import mock 39 | except ImportError: 40 | import mock 41 | 42 | try: 43 | from urllib.parse import urlparse, quote 44 | except ImportError: 45 | from urlparse import urlparse 46 | from urllib import quote 47 | 48 | import adal 49 | from tests import util 50 | from tests.util import parameters as cp 51 | 52 | class TestUserRealm(unittest.TestCase): 53 | 54 | def setUp(self): 55 | self.authority = 'https://login.microsoftonline.com' 56 | self.user = 'test@federatedtenant-com' 57 | 58 | user_realm_path = cp['userRealmPathTemplate'].replace('', quote(self.user, safe='~()*!.\'')) 59 | query = 'api-version=1.0' 60 | self.testUrl = self.authority + user_realm_path + '?' + query 61 | 62 | return super(TestUserRealm, self).setUp() 63 | 64 | @httpretty.activate 65 | def test_happy_path_federated(self): 66 | 67 | user_realm_response = '{\"account_type\":\"Federated\",\"federation_protocol\":\"wstrust\",\"federation_metadata_url\":\"https://adfs.federatedtenant.com/adfs/services/trust/mex\",\"federation_active_auth_url\":\"https://adfs.federatedtenant.com/adfs/services/trust/2005/usernamemixed\",\"ver\":\"0.8\"}' 68 | 69 | httpretty.register_uri(httpretty.GET, uri=self.testUrl, body=user_realm_response, status=200) 70 | user_realm = adal.user_realm.UserRealm(cp['callContext'], self.user, self.authority) 71 | 72 | try: 73 | user_realm.discover() 74 | self.assertEqual(user_realm.federation_metadata_url, 'https://adfs.federatedtenant.com/adfs/services/trust/mex', 75 | 'Returned Mex URL does not match expected value: {0}'.format(user_realm.federation_metadata_url)) 76 | self.assertAlmostEqual(user_realm.federation_active_auth_url, 'https://adfs.federatedtenant.com/adfs/services/trust/2005/usernamemixed', 77 | 'Returned active auth URL does not match expected value: {0}'.format(user_realm.federation_active_auth_url)) 78 | except Exception as exp: 79 | self.assertIsNone(exp, "Error raised during function: {0}".format(exp)) 80 | 81 | util.match_standard_request_headers(httpretty.last_request()) 82 | 83 | @httpretty.activate 84 | def test_negative_wrong_field(self): 85 | 86 | user_realm_response = '{\"account_type\":\"Manageddf\",\"federation_protocol\":\"SAML20fgfg\",\"federation_metadata\":\"https://adfs.federatedtenant.com/adfs/services/trust/mex\",\"federation_active_auth_url\":\"https://adfs.federatedtenant.com/adfs/services/trust/2005/usernamemixed\",\"version\":\"0.8\"}' 87 | 88 | httpretty.register_uri(httpretty.GET, uri=self.testUrl, body=user_realm_response, status=200) 89 | user_realm = adal.user_realm.UserRealm(cp['callContext'], self.user, self.authority) 90 | 91 | try: 92 | user_realm.discover() 93 | except Exception as exp: 94 | receivedException = True 95 | pass 96 | finally: 97 | self.assertTrue(receivedException,'Did not receive expected error') 98 | util.match_standard_request_headers(httpretty.last_request()) 99 | 100 | @httpretty.activate 101 | def test_negative_no_root(self): 102 | 103 | user_realm_response = 'noroot' 104 | 105 | httpretty.register_uri(httpretty.GET, uri=self.testUrl, body=user_realm_response, status=200) 106 | user_realm = adal.user_realm.UserRealm(cp['callContext'], self.user, self.authority) 107 | 108 | try: 109 | user_realm.discover() 110 | except Exception as exp: 111 | receivedException = True 112 | pass 113 | finally: 114 | self.assertTrue(receivedException,'Did not receive expected error') 115 | util.match_standard_request_headers(httpretty.last_request()) 116 | 117 | @httpretty.activate 118 | def test_negative_empty_json(self): 119 | 120 | user_realm_response = '{}' 121 | 122 | httpretty.register_uri(httpretty.GET, uri=self.testUrl, body=user_realm_response, status=200) 123 | user_realm = adal.user_realm.UserRealm(cp['callContext'], self.user, self.authority) 124 | 125 | try: 126 | user_realm.discover() 127 | except Exception as exp: 128 | receivedException = True 129 | pass 130 | finally: 131 | self.assertTrue(receivedException,'Did not receive expected error') 132 | util.match_standard_request_headers(httpretty.last_request()) 133 | 134 | @httpretty.activate 135 | def test_negative_fed_err(self): 136 | 137 | user_realm_response = '{\"account_type\":\"Federated\",\"federation_protocol\":\"wstrustww\",\"federation_metadata_url\":\"https://adfs.federatedtenant.com/adfs/services/trust/mex\",\"federation_active_auth_url\":\"https://adfs.federatedtenant.com/adfs/services/trust/2005/usernamemixed\",\"ver\":\"0.8\"}' 138 | 139 | httpretty.register_uri(httpretty.GET, uri=self.testUrl, body=user_realm_response, status=200) 140 | user_realm = adal.user_realm.UserRealm(cp['callContext'], self.user, self.authority) 141 | 142 | try: 143 | user_realm.discover() 144 | except Exception as exp: 145 | receivedException = True 146 | pass 147 | finally: 148 | self.assertTrue(receivedException,'Did not receive expected error') 149 | util.match_standard_request_headers(httpretty.last_request()) 150 | 151 | if __name__ == '__main__': 152 | unittest.main() 153 | -------------------------------------------------------------------------------- /tests/test_wstrust_request.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | import os 28 | import unittest 29 | import httpretty 30 | 31 | try: 32 | from unittest import mock 33 | except ImportError: 34 | import mock 35 | 36 | from adal.wstrust_request import WSTrustRequest 37 | from adal.wstrust_response import WSTrustResponse 38 | from adal.constants import WSTrustVersion 39 | 40 | TEST_CORRELATION_ID = 'test-correlation-id-123456789' 41 | wstrustEndpoint = 'https://test.wstrust.endpoint/' 42 | _call_context = { 'log_context' : {'correlation_id': TEST_CORRELATION_ID } } 43 | 44 | class Test_wstrust_request(unittest.TestCase): 45 | 46 | @httpretty.activate 47 | def test_happy_path(self): 48 | username = 'test_username' 49 | password = 'test_password' 50 | appliesTo = 'test_appliesTo' 51 | wstrustFile = open(os.path.join(os.getcwd(), 'tests', 'wstrust', 'RST.xml'), mode='r') 52 | templateRST = wstrustFile.read() 53 | rst = templateRST \ 54 | .replace('%USERNAME%', username) \ 55 | .replace('%PASSWORD%', password) \ 56 | .replace('%APPLIES_TO%', appliesTo) \ 57 | .replace('%WSTRUST_ENDPOINT%', wstrustEndpoint) 58 | 59 | #rstRequest = setupUpOutgoingRSTCompare(rst) 60 | request = WSTrustRequest(_call_context, wstrustEndpoint, appliesTo, WSTrustVersion.WSTRUST13) 61 | 62 | # TODO: handle rstr should be mocked out to prevent handling here. 63 | # TODO: setupUpOutgoingRSTCompare. Use this to get messageid, created, expires, etc comparisons. 64 | 65 | httpretty.register_uri(method=httpretty.POST, uri=wstrustEndpoint, status=200, body='') 66 | 67 | request._handle_rstr =mock.MagicMock() 68 | 69 | request.acquire_token(username, password) 70 | wstrustFile.close() 71 | 72 | @httpretty.activate 73 | def test_fail_to_parse_rstr(self): 74 | username = 'test_username' 75 | password = 'test_password' 76 | appliesTo = 'test_appliesTo' 77 | templateFile = open(os.path.join(os.getcwd(), 'tests', 'wstrust', 'RST.xml'), mode='r') 78 | templateRST = templateFile.read() 79 | templateFile.close() 80 | rst = templateRST \ 81 | .replace('%USERNAME%', username) \ 82 | .replace('%PASSWORD%', password) \ 83 | .replace('%APPLIES_TO%', appliesTo) \ 84 | .replace('%WSTRUST_ENDPOINT%', wstrustEndpoint) 85 | 86 | httpretty.register_uri(method=httpretty.POST, uri=wstrustEndpoint, status=200, body='fake response body') 87 | 88 | request = WSTrustRequest(_call_context, wstrustEndpoint, appliesTo, WSTrustVersion.WSTRUST13) 89 | with self.assertRaises(Exception): 90 | request.acquire_token(username, password) 91 | 92 | 93 | if __name__ == '__main__': 94 | unittest.main() 95 | -------------------------------------------------------------------------------- /tests/test_wstrust_response.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # 3 | # Copyright (c) Microsoft Corporation. 4 | # All rights reserved. 5 | # 6 | # This code is licensed under the MIT License. 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files(the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions : 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | #------------------------------------------------------------------------------ 27 | 28 | import unittest 29 | import os 30 | import six 31 | 32 | try: 33 | from xml.etree import cElementTree as ET 34 | except ImportError: 35 | from xml.etree import ElementTree as ET 36 | 37 | from adal.constants import XmlNamespaces, Errors, WSTrustVersion 38 | from adal.wstrust_response import WSTrustResponse 39 | from adal.wstrust_response import findall_content 40 | 41 | _namespaces = XmlNamespaces.namespaces 42 | _call_context = {'log_context' : {'correlation-id':'test-corr-id'}} 43 | 44 | class Test_wstrustresponse(unittest.TestCase): 45 | def test_parse_error_happy_path(self): 46 | errorResponse = ''' 47 | 48 | 49 | http://www.w3.org/2005/08/addressing/soap/fault 50 | 51 | 52 | 2013-07-30T00:32:21.989Z 53 | 2013-07-30T00:37:21.989Z 54 | 55 | 56 | 57 | 58 | 59 | 60 | s:Sender 61 | 62 | a:RequestFailed 63 | 64 | 65 | 66 | MSIS3127: The specified request failed. 67 | 68 | 69 | 70 | ''' 71 | 72 | wstrustResponse = WSTrustResponse(_call_context, errorResponse, WSTrustVersion.WSTRUST13) 73 | 74 | exception_text = "Server returned error in RSTR - ErrorCode: RequestFailed : FaultMessage: MSIS3127: The specified request failed" 75 | with six.assertRaisesRegex(self, Exception, exception_text) as cm: 76 | wstrustResponse.parse() 77 | 78 | def test_token_parsing_happy_path(self): 79 | wstrustFile = open(os.path.join(os.getcwd(), 'tests', 'wstrust', 'RSTR.xml')) 80 | wstrustResponse = WSTrustResponse(_call_context, wstrustFile.read(), WSTrustVersion.WSTRUST13) 81 | wstrustResponse.parse() 82 | wstrustFile.close() 83 | 84 | self.assertEqual(wstrustResponse.token_type, 'urn:oasis:names:tc:SAML:1.0:assertion', 'TokenType did not match expected value: ' + wstrustResponse.token_type) 85 | 86 | attribute_values = ET.fromstring(wstrustResponse.token).findall('saml:AttributeStatement/saml:Attribute/saml:AttributeValue', _namespaces) 87 | self.assertEqual(2, len(attribute_values)) 88 | self.assertEqual('1TIu064jGEmmf+hnI+F0Jg==', attribute_values[1].text) 89 | 90 | def test_rstr_none(self): 91 | with six.assertRaisesRegex(self, Exception, 'Received empty RSTR response body.') as cm: 92 | wstrustResponse = WSTrustResponse(_call_context, None, WSTrustVersion.WSTRUST13) 93 | wstrustResponse.parse() 94 | 95 | def test_rstr_empty_string(self): 96 | with six.assertRaisesRegex(self, Exception, 'Received empty RSTR response body.') as cm: 97 | wstrustResponse = WSTrustResponse(_call_context, '', WSTrustVersion.WSTRUST13) 98 | wstrustResponse.parse() 99 | 100 | def test_rstr_unparseable_xml(self): 101 | with six.assertRaisesRegex(self, Exception, 'Failed to parse RSTR in to DOM'): 102 | wstrustResponse = WSTrustResponse(_call_context, ' 108 | 109 | foo 110 | 111 | """ 112 | sample = ('' 113 | + content 114 | + '') 115 | 116 | # Demonstrating how XML-based parser won't give you the raw content as-is 117 | element = ET.fromstring(sample).findall('{SAML:assertion}Assertion')[0] 118 | assertion_via_xml_parser = ET.tostring(element) 119 | self.assertNotEqual(content, assertion_via_xml_parser) 120 | self.assertNotIn(b"", assertion_via_xml_parser) 121 | 122 | # The findall_content() helper, based on Regex, will return content as-is. 123 | self.assertEqual([content], findall_content(sample, "Wrapper")) 124 | 125 | def test_findall_content_for_real(self): 126 | with open(os.path.join(os.getcwd(), 'tests', 'wstrust', 'RSTR.xml')) as f: 127 | rstr = f.read() 128 | wstrustResponse = WSTrustResponse(_call_context, rstr, WSTrustVersion.WSTRUST13) 129 | wstrustResponse.parse() 130 | self.assertIn("", rstr) 131 | self.assertIn(b"", wstrustResponse.token) # It is in bytes 132 | 133 | if __name__ == '__main__': 134 | unittest.main() 135 | -------------------------------------------------------------------------------- /tests/wstrust/RST.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue 4 | %MESSAGE_ID% 5 | 6 | http://www.w3.org/2005/08/addressing/anonymous 7 | 8 | %WSTRUST_ENDPOINT% 9 | 10 | 11 | %CREATED% 12 | %EXPIRES% 13 | 14 | 15 | %USERNAME% 16 | %PASSWORD% 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | %APPLIES_TO% 25 | 26 | 27 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer 28 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/wstrust/RSTR.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal 4 | 5 | 6 | 2013-11-15T03:08:25.221Z 7 | 2013-11-15T03:13:25.221Z 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 2013-11-15T03:08:25.205Z 16 | 2013-11-15T04:08:25.205Z 17 | 18 | 19 | 20 | https://login.microsoftonline.com/extSTS.srf 21 | 22 | 23 | 24 | 25 | 26 | 27 | https://login.microsoftonline.com/extSTS.srf 28 | 29 | 30 | 31 | 32 | 1TIu064jGEmmf+hnI+F0Jg== 33 | 34 | urn:oasis:names:tc:SAML:1.0:cm:bearer 35 | 36 | 37 | 38 | frizzo@richard-randall.com 39 | 40 | 41 | 1TIu064jGEmmf+hnI+F0Jg== 42 | 43 | 44 | 45 | 46 | 1TIu064jGEmmf+hnI+F0Jg== 47 | 48 | urn:oasis:names:tc:SAML:1.0:cm:bearer 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 3i95D+nRbsyRitSPeT7ZtEr5vbM= 63 | 64 | 65 | aVNmmKLNdAlBxxcNciWVfxynZUPR9ql8ZZSZt/qpqL/GB3HX/cL/QnfG2OOKrmhgEaR0Ul4grZhGJxlxMPDL0fhnBz+VJ5HwztMFgMYs3Md8A2sZd9n4dfu7+CByAna06lCwwfdFWlNV1MBFvlWvYtCLNkpYVr/aglmb9zpMkNxEOmHe/cwxUtYlzH4RpIsIT5pruoJtUxKcqTRDEeeYdzjBAiJuguQTChLmHNoMPdX1RmtJlPsrZ1s9R/IJky7fHLjB7jiTDceRCS5QUbgUqYbLG1MjFXthY2Hr7K9kpYjxxIk6xmM7mFQE3Hts3bj6UU7ElUvHpX9bxxk3pqzlhg== 66 | 67 | 68 | MIIC6DCCAdCgAwIBAgIQaztYF2TpvZZG6yreA3NRpzANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVBREZTIFNpZ25pbmcgLSBmcy5yaWNoYXJkLXJhbmRhbGwuY29tMB4XDTEzMTExMTAzNTMwMFoXDTE0MTExMTAzNTMwMFowMDEuMCwGA1UEAxMlQURGUyBTaWduaW5nIC0gZnMucmljaGFyZC1yYW5kYWxsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO+1VWY/sYDdN3hdsvT+mWHTcOwjp2G9e0AEZdmgh7bS54WUJw9y0cMxJmGB0jAAW40zomzIbS8/o3iuxcJyFgBVtMFfXwFjVQJnZJ7IMXFs1V/pJHrwWHxePz/WzXFtMaqEIe8QummJ07UBg9UsYZUYTGO9NDGw1Yr/oRNsl7bLA0S/QlW6yryf6l3snHzIgtO2xiWn6q3vCJTTVNMROkI2YKNKdYiD5fFD77kFACfJmOwP8MN9u+HM2IN6g0Nv5s7rMyw077Co/xKefamWQCB0jLpv89jo3hLgkwIgWX4cMVgHSNmdzXSgC3owG8ivRuJDATh83GiqI6jzA1+x4rkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAxA5MQZHw9lJYDpU4f45EYrWPEaAPnncaoxIeLE9fG14gA01frajRfdyoO0AKqb+ZG6sePKngsuq4QHA2EnEI4Di5uWKsXy1Id0AXUSUhLpe63alZ8OwiNKDKn71nwpXnlGwKqljnG3xBMniGtGKrFS4WM+joEHzaKpvgtGRGoDdtXF4UXZJcn2maw6d/kiHrQ3kWoQcQcJ9hVIo8bC0BPvxV0Qh4TF3Nb3tKhaXsY68eMxMGbHok9trVHQ3Vew35FuTg1JzsfCFSDF8sxu7FJ4iZ7VLM8MQLnvIMcubLJvc57EHSsNyeiqBFQIYkdg7MSf+Ot2qJjfExgo+NOtWN+g== 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | _9bd2b280-f153-471a-9b73-c1df0d555075 77 | 78 | 79 | 80 | 81 | _9bd2b280-f153-471a-9b73-c1df0d555075 82 | 83 | 84 | urn:oasis:names:tc:SAML:1.0:assertion 85 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue 86 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /tests/wstrust/common.base64.encoded.assertion.txt: -------------------------------------------------------------------------------- 1 | PHNhbWw6QXNzZXJ0aW9uIE1ham9yVmVyc2lvbj0iMSIgTWlub3JWZXJzaW9uPSIxIiBBc3NlcnRpb25JRD0iX2JmMTM3ZjkwLTdkZDctNDY2OC04YTM5LThiZjU1ZWI1MjAxNyIgSXNzdWVyPSJodHRwOi8vZnMubmF0dXJhbGNhdXNlcy5jb20vYWRmcy9zZXJ2aWNlcy90cnVzdCIgSXNzdWVJbnN0YW50PSIyMDE0LTAxLTI3VDA4OjE1OjQ1LjAwM1oiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMDphc3NlcnRpb24iPjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAxLTI3VDA4OjE1OjQ1LjAwM1oiIE5vdE9uT3JBZnRlcj0iMjAxNC0wMS0yN1QwOToxNTo0NS4wMDNaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uQ29uZGl0aW9uPjxzYW1sOkF1ZGllbmNlPnVybjpmZWRlcmF0aW9uOk1pY3Jvc29mdE9ubGluZTwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbkNvbmRpdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSWRlbnRpZmllciBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIj40M1lSelRyaCtVZThlVGVjbnFMQXhRPT08L3NhbWw6TmFtZUlkZW50aWZpZXI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48c2FtbDpDb25maXJtYXRpb25NZXRob2Q+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4wOmNtOmJlYXJlcjwvc2FtbDpDb25maXJtYXRpb25NZXRob2Q+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6QXR0cmlidXRlIEF0dHJpYnV0ZU5hbWU9IlVQTiIgQXR0cmlidXRlTmFtZXNwYWNlPSJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy9jbGFpbXMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlPmZyaXp6b0BuYXR1cmFsY2F1c2VzLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBBdHRyaWJ1dGVOYW1lPSJJbW11dGFibGVJRCIgQXR0cmlidXRlTmFtZXNwYWNlPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL0xpdmVJRC9GZWRlcmF0aW9uLzIwMDgvMDUiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlPjQzWVJ6VHJoK1VlOGVUZWNucUxBeFE9PTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXV0aGVudGljYXRpb25TdGF0ZW1lbnQgQXV0aGVudGljYXRpb25NZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMDphbTpwYXNzd29yZCIgQXV0aGVudGljYXRpb25JbnN0YW50PSIyMDE0LTAxLTI3VDA4OjE1OjQ0Ljk4N1oiPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlkZW50aWZpZXIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDp1bnNwZWNpZmllZCI+NDNZUnpUcmgrVWU4ZVRlY25xTEF4UT09PC9zYW1sOk5hbWVJZGVudGlmaWVyPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24+PHNhbWw6Q29uZmlybWF0aW9uTWV0aG9kPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMDpjbTpiZWFyZXI8L3NhbWw6Q29uZmlybWF0aW9uTWV0aG9kPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0Pjwvc2FtbDpBdXRoZW50aWNhdGlvblN0YXRlbWVudD48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz48ZHM6UmVmZXJlbmNlIFVSST0iI19iZjEzN2Y5MC03ZGQ3LTQ2NjgtOGEzOS04YmY1NWViNTIwMTciPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPkI1ZUVJa1RoZ1M1NDQ1WkJaYVdXYmlxM1Vtdz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+Yk0vQUVMOElzcWs3a0d4RFB2Y2lDN1JsbXI5ZmZSV0doa25wUmM0TFVDMEJqUWlZb28xMU5ZN3hoMFM1QSt4S0lvWE1nZWFhUDZxamIwbjI3VE5UM2craE9jQityOXg2SDVoQU5zNHJ0bC91T0VNMHBLdmNnQlkwYmh6NEhEUHFhaVJwQVZJdGdpU0dudERJZWc0MmNPaFNZSjlPbjZvR1FjVkE1aHkyR210eHhrN2Q3YzRTcTJueW0zdDNEM0RMTU9md3ZsdmlCWnNkQmdsVWc0aVpyUHU4cWdUUnJVbmZHaTVNMVZzVHVTVHdLVng3TkJOTGZqQnhBaXBtck0wUHlPSWs0M0NIQzNMSmpUQ3phajd2UlNxQmJYRjNMSW0yRGMwSFVMYlViZ3dPS3JvNzhyN2JkQ0Yzc0xmYlUyd2dUWnl2SWtYN0I3K29vdnN3RjZHb3l3PT08L2RzOlNpZ25hdHVyZVZhbHVlPjxLZXlJbmZvIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48WDUwOURhdGE+PFg1MDlDZXJ0aWZpY2F0ZT5NSUlDNURDQ0FjeWdBd0lCQWdJUUdWTHpkbnZrQWFaSFp1ZlVpN3p2cVRBTkJna3Foa2lHOXcwQkFRc0ZBREF1TVN3d0tnWURWUVFERXlOQlJFWlRJRk5wWjI1cGJtY2dMU0JtY3k1dVlYUjFjbUZzWTJGMWMyVnpMbU52YlRBZUZ3MHhNekV4TVRBd01qRXhORGxhRncweE5ERXhNVEF3TWpFeE5EbGFNQzR4TERBcUJnTlZCQU1USTBGRVJsTWdVMmxuYm1sdVp5QXRJR1p6TG01aGRIVnlZV3hqWVhWelpYTXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF2c0psY2ttWjRqWkVKREJISG1kSDBDbGdPNENwNWJ2Z0FlTmVKTFFrSGxybkhEME81L2JreU1vNGFOZy9Gb0pKY0V0MFh6emQ4MTZsbkRZek0yVVRhV0Foa0hOYmw0c1ZXNGR0TFhlS0hlR3l5NDNVYlJZWWhJcERJSzhOaWN5UmRoeE5ualhiWkVNY3VROG5YcmJrajNETW5sQkVNLzVocFM3MzMreVZZclVrN0JjbXhhYjFsRFJVT0xiTDVLaDM1RzJKa1g4aUN4elVySFZqMTVEbmVHVlFHeUZPbWYyRHBDOUNOZXAxMjNYWWRYT2Z0WHQ0TmgxKy9lZDExemplWlhlUThobjM2LzNOSituNG1Ja0JlREdYbWhCZGg4TUFaV0NnVXgzRytTZGlmQzNiVlUzQnJWV2VQb2NUaUg2aGQxbGpMMmNWaDdoaEVJT3RHSEQ0S3dJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNESXRobml2MUhrL05qSGtsQlhvVElYUFhXZVNhNThaWmZ0b3IwbzBxbHFaWDFoQWx1WHhKZUxLV3R1aUpLa1pvL2VUam9QaWRPM0wvWDBwc1pSK2NtTXNiRUE5dHRBTzhBa3FldVl4amx3MFlrZnFyWDZ5Uk5sa0dXcjRTVTA3WmdmSmhvVHNDUDlvT0JHL3gyeERrQUdmQUVkeUJ5RXZHa2F0MTZIZjJFTEEzZm9DcVVXOE1HSk0xNklEbXU2SzlLckdBT1IzZGEwUHdRNC9zRVRIa2gxQWc1amtZNjlsSjdyM01nemRNTVpEYzh0UFFhelZaYmUweGMxdThXRzUyYzJVR29heTl6TnFNUUpPR25VWk5COWZWSGF6QTJwdk1oQmJ5QlNxbzFqUzMxQlhMbG9kN3Y1TkVNOEY1QUdpa1V3WUhWK0VaL08ydDQ5MWdjRmpjOTwvWDUwOUNlcnRpZmljYXRlPjwvWDUwOURhdGE+PC9LZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjwvc2FtbDpBc3NlcnRpb24+ -------------------------------------------------------------------------------- /tests/wstrust/common.rstr.xml: -------------------------------------------------------------------------------- 1 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal2014-01-27T08:15:45.003Z2014-01-27T08:20:45.003Z2014-01-27T08:15:45.003Z2014-01-27T09:15:45.003Zurn:federation:MicrosoftOnlineurn:federation:MicrosoftOnline43YRzTrh+Ue8eTecnqLAxQ==urn:oasis:names:tc:SAML:1.0:cm:bearerfrizzo@naturalcauses.com43YRzTrh+Ue8eTecnqLAxQ==43YRzTrh+Ue8eTecnqLAxQ==urn:oasis:names:tc:SAML:1.0:cm:bearerB5eEIkThgS5445ZBZaWWbiq3Umw=bM/AEL8Isqk7kGxDPvciC7Rlmr9ffRWGhknpRc4LUC0BjQiYoo11NY7xh0S5A+xKIoXMgeaaP6qjb0n27TNT3g+hOcB+r9x6H5hANs4rtl/uOEM0pKvcgBY0bhz4HDPqaiRpAVItgiSGntDIeg42cOhSYJ9On6oGQcVA5hy2Gmtxxk7d7c4Sq2nym3t3D3DLMOfwvlviBZsdBglUg4iZrPu8qgTRrUnfGi5M1VsTuSTwKVx7NBNLfjBxAipmrM0PyOIk43CHC3LJjTCzaj7vRSqBbXF3LIm2Dc0HULbUbgwOKro78r7bdCF3sLfbU2wgTZyvIkX7B7+oovswF6Goyw==MIIC5DCCAcygAwIBAgIQGVLzdnvkAaZHZufUi7zvqTANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNBREZTIFNpZ25pbmcgLSBmcy5uYXR1cmFsY2F1c2VzLmNvbTAeFw0xMzExMTAwMjExNDlaFw0xNDExMTAwMjExNDlaMC4xLDAqBgNVBAMTI0FERlMgU2lnbmluZyAtIGZzLm5hdHVyYWxjYXVzZXMuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvsJlckmZ4jZEJDBHHmdH0ClgO4Cp5bvgAeNeJLQkHlrnHD0O5/bkyMo4aNg/FoJJcEt0Xzzd816lnDYzM2UTaWAhkHNbl4sVW4dtLXeKHeGyy43UbRYYhIpDIK8NicyRdhxNnjXbZEMcuQ8nXrbkj3DMnlBEM/5hpS733+yVYrUk7Bcmxab1lDRUOLbL5Kh35G2JkX8iCxzUrHVj15DneGVQGyFOmf2DpC9CNep123XYdXOftXt4Nh1+/ed11zjeZXeQ8hn36/3NJ+n4mIkBeDGXmhBdh8MAZWCgUx3G+SdifC3bVU3BrVWePocTiH6hd1ljL2cVh7hhEIOtGHD4KwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCDIthniv1Hk/NjHklBXoTIXPXWeSa58ZZftor0o0qlqZX1hAluXxJeLKWtuiJKkZo/eTjoPidO3L/X0psZR+cmMsbEA9ttAO8AkqeuYxjlw0YkfqrX6yRNlkGWr4SU07ZgfJhoTsCP9oOBG/x2xDkAGfAEdyByEvGkat16Hf2ELA3foCqUW8MGJM16IDmu6K9KrGAOR3da0PwQ4/sETHkh1Ag5jkY69lJ7r3MgzdMMZDc8tPQazVZbe0xc1u8WG52c2UGoay9zNqMQJOGnUZNB9fVHazA2pvMhBbyBSqo1jS31BXLlod7v5NEM8F5AGikUwYHV+EZ/O2t491gcFjc9_bf137f90-7dd7-4668-8a39-8bf55eb52017_bf137f90-7dd7-4668-8a39-8bf55eb52017urn:oasis:names:tc:SAML:1.0:assertionhttp://docs.oasis-open.org/ws-sx/ws-trust/200512/Issuehttp://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer --------------------------------------------------------------------------------