├── .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 | [](https://travis-ci.org/AzureAD/azure-activedirectory-library-for-python) | [](https://travis-ci.org/AzureAD/azure-activedirectory-library-for-python) | [](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
--------------------------------------------------------------------------------