pax_global_header00006660000000000000000000000064136124236250014516gustar00rootroot0000000000000052 comment=da09f2573ad0e8e58ef25f53efe49be0bec95f42 microsoft-authentication-library-for-python-1.1.0/000077500000000000000000000000001361242362500223045ustar00rootroot00000000000000microsoft-authentication-library-for-python-1.1.0/.gitignore000066400000000000000000000012651361242362500243000ustar00rootroot00000000000000# Python cache __pycache__/ *.pyc # PTVS analysis .ptvs/ *.pyproj # Build results /bin/ /obj/ /dist/ /MANIFEST # Result of running python setup.py install/pip install -e /build/ /msal.egg-info/ # Test results /TestResults/ # User-specific files *.suo *.user *.sln.docstates /tests/config.py # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Mac desktop service store files .DS_Store .idea src/build *.iml /doc/_build # Virtual Environments /env* # Visual Studio Files /.vs/* /tests/.vs/* # vim files *.swp # The test configuration file(s) could potentially contain credentials tests/config.json microsoft-authentication-library-for-python-1.1.0/.travis.yml000066400000000000000000000043761361242362500244270ustar00rootroot00000000000000sudo: false language: python python: - "2.7" - "3.5" - "3.6" # Borrowed from https://github.com/travis-ci/travis-ci/issues/9815 # Enable 3.7 without globally enabling sudo and dist: xenial for other build jobs matrix: include: - python: 3.7 dist: xenial sudo: true - python: 3.8 dist: xenial sudo: true install: - pip install -r requirements.txt script: - python -m unittest discover -s tests deploy: - # test pypi provider: pypi distributions: "sdist bdist_wheel" server: https://test.pypi.org/legacy/ user: "nugetaad" password: secure: KkjKySJujYxx31B15mlAZr2Jo4P99LcrMj3uON/X/WMXAqYVcVsYJ6JSzUvpNnCAgk+1hc24Qp6nibQHV824yiK+eG4qV+lpzkEEedkRx6NOW/h09OkT+pOSVMs0kcIhz7FzqChpl+jf6ZZpb13yJpQg2LoZIA4g8UdYHHFidWt4m5u1FZ9LPCqQ0OT3gnKK4qb0HIDaECfz5GYzrelLLces0PPwj1+X5eb38xUVtbkA1UJKLGKI882D8Rq5eBdbnDGsfDnF6oU+EBnGZ7o6HVQLdBgagDoVdx7yoXyntULeNxTENMTOZJEJbncQwxRgeEqJWXTTEW57O6Jo5uiHEpJA9lAePlRbS+z6BPDlnQogqOdTsYS0XMfOpYE0/r3cbtPUjETOmGYQxjQzfrFBfM7jaWnUquymZRYqCQ66VDo3I/ykNOCoM9qTmWt5L/MFfOZyoxLHnDThZBdJ3GXHfbivg+v+vOfY1gG8e2H2lQY+/LIMIJibF+MS4lJgrB81dcNdBzyxMNByuWQjSL1TY7un0QzcRcZz2NLrFGg8+9d67LQq4mK5ySimc6zdgnanuROU02vGr1EApT6D/qUItiulFgWqInNKrFXE9q74UP/WSooZPoLa3Du8y5s4eKerYYHQy5eSfIC8xKKDU8MSgoZhwQhCUP46G9Nsty0PYQc= on: branch: master tags: false condition: $TRAVIS_PYTHON_VERSION = "2.7" - # production pypi provider: pypi distributions: "sdist bdist_wheel" user: "nugetaad" password: secure: KkjKySJujYxx31B15mlAZr2Jo4P99LcrMj3uON/X/WMXAqYVcVsYJ6JSzUvpNnCAgk+1hc24Qp6nibQHV824yiK+eG4qV+lpzkEEedkRx6NOW/h09OkT+pOSVMs0kcIhz7FzqChpl+jf6ZZpb13yJpQg2LoZIA4g8UdYHHFidWt4m5u1FZ9LPCqQ0OT3gnKK4qb0HIDaECfz5GYzrelLLces0PPwj1+X5eb38xUVtbkA1UJKLGKI882D8Rq5eBdbnDGsfDnF6oU+EBnGZ7o6HVQLdBgagDoVdx7yoXyntULeNxTENMTOZJEJbncQwxRgeEqJWXTTEW57O6Jo5uiHEpJA9lAePlRbS+z6BPDlnQogqOdTsYS0XMfOpYE0/r3cbtPUjETOmGYQxjQzfrFBfM7jaWnUquymZRYqCQ66VDo3I/ykNOCoM9qTmWt5L/MFfOZyoxLHnDThZBdJ3GXHfbivg+v+vOfY1gG8e2H2lQY+/LIMIJibF+MS4lJgrB81dcNdBzyxMNByuWQjSL1TY7un0QzcRcZz2NLrFGg8+9d67LQq4mK5ySimc6zdgnanuROU02vGr1EApT6D/qUItiulFgWqInNKrFXE9q74UP/WSooZPoLa3Du8y5s4eKerYYHQy5eSfIC8xKKDU8MSgoZhwQhCUP46G9Nsty0PYQc= on: branch: master tags: true condition: $TRAVIS_PYTHON_VERSION = "2.7" microsoft-authentication-library-for-python-1.1.0/LICENSE000066400000000000000000000022001361242362500233030ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) Microsoft Corporation. All rights reserved. This code is licensed under the MIT License. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions : The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.microsoft-authentication-library-for-python-1.1.0/README.md000066400000000000000000000177201361242362500235720ustar00rootroot00000000000000# Microsoft Authentication Library (MSAL) for Python | `dev` branch | Reference Docs |---------------|--------------- [![Build status](https://api.travis-ci.org/AzureAD/microsoft-authentication-library-for-python.svg?branch=dev)](https://travis-ci.org/AzureAD/microsoft-authentication-library-for-python) | [![Documentation Status](https://readthedocs.org/projects/msal-python/badge/?version=latest)](https://msal-python.readthedocs.io/en/latest/?badge=latest) The Microsoft Authentication Library for Python enables applications to integrate with the [Microsoft identity platform](https://aka.ms/aaddevv2). It allows you to sign in users or apps with Microsoft identities ([Azure AD](https://azure.microsoft.com/services/active-directory/), [Microsoft Accounts](https://account.microsoft.com) and [Azure AD B2C](https://azure.microsoft.com/services/active-directory-b2c/) accounts) and obtain tokens to call Microsoft APIs such as [Microsoft Graph](https://graph.microsoft.io/) or your own APIs registered with the Microsoft identity platform. It is built using industry standard OAuth2 and OpenID Connect protocols Not sure whether this is the SDK you are looking for your app? There are other Microsoft Identity SDKs [here](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Microsoft-Authentication-Client-Libraries). Quick links: | [Getting Started](https://docs.microsoft.com/azure/active-directory/develop/quickstart-v2-python-webapp) | [Docs](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki) | [Samples](https://aka.ms/aaddevsamplesv2) | [Support](README.md#community-help-and-support) | --- | --- | --- | --- | ## Installation You can find MSAL Python on [Pypi](https://pypi.org/project/msal/). 1. If you haven't already, [install and/or upgrade the pip](https://pip.pypa.io/en/stable/installing/) of your Python environment to a recent version. We tested with pip 18.1. 2. As usual, just run `pip install msal`. ## Versions This library follows [Semantic Versioning](http://semver.org/). You can find the changes for each version under [Releases](https://github.com/AzureAD/microsoft-authentication-library-for-python/releases). ## Usage Before using MSAL Python (or any MSAL SDKs, for that matter), you will have to [register your application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-v2-register-an-app). Acquiring tokens with MSAL Python follows this 3-step pattern. 1. MSAL proposes a clean separation between [public client applications, and confidential client applications](https://tools.ietf.org/html/rfc6749#section-2.1). So you will first create either a `PublicClientApplication` or a `ConfidentialClientApplication` instance, and ideally reuse it during the lifecycle of your app. The following example shows a `PublicClientApplication`: ```python from msal import PublicClientApplication app = PublicClientApplication("your_client_id", authority="...") ``` Later, each time you would want an access token, you start by: ```python result = None # It is just an initial value. Please follow instructions below. ``` 2. The API model in MSAL provides you explicit control on how to utilize token cache. This cache part is technically optional, but we highly recommend you to harness the power of MSAL cache. It will automatically handle the token refresh for you. ```python # We now check the cache to see # whether we already have some accounts that the end user already used to sign in before. accounts = app.get_accounts() if accounts: # If so, you could then somehow display these accounts and let end user choose print("Pick the account you want to use to proceed:") for a in accounts: print(a["username"]) # Assuming the end user chose this one chosen = accounts[0] # Now let's try to find a token in cache for this account result = app.acquire_token_silent(config["scope"], account=chosen) ``` 3. Either there is no suitable token in the cache, or you chose to skip the previous step, now it is time to actually send a request to AAD to obtain a token. There are different methods based on your client type and scenario. Here we demonstrate a placeholder flow. ```python if not result: # So no suitable token exists in cache. Let's get a new one from AAD. result = app.acquire_token_by_one_of_the_actual_method(..., scopes=["User.Read"]) if "access_token" in result: print(result["access_token"]) # Yay! else: print(result.get("error")) print(result.get("error_description")) print(result.get("correlation_id")) # You may need this when reporting a bug ``` That is the high level pattern. There will be some variations for different flows. They are demonstrated in [samples hosted right in this repo](https://github.com/AzureAD/microsoft-authentication-library-for-python/tree/dev/sample). Refer the [Wiki](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki) pages for more details on the MSAL Python functionality and usage. ## Migrating from ADAL If your application is using ADAL Python, we recommend you to update to use MSAL Python. No new feature work will be done in ADAL Python. See the [ADAL to MSAL migration](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Migrate-to-MSAL-Python) guide. ## Roadmap You can follow the latest updates and plans for MSAL Python in the [Roadmap](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Roadmap) published on our Wiki. ## Samples and Documentation MSAL Python supports multiple [application types and authentication scenarios](https://docs.microsoft.com/azure/active-directory/develop/authentication-flows-app-scenarios). The generic documents on [Auth Scenarios](https://docs.microsoft.com/azure/active-directory/develop/authentication-scenarios) and [Auth protocols](https://docs.microsoft.com/azure/active-directory/develop/active-directory-v2-protocols) are recommended reading. We provide a [full suite of sample applications](https://aka.ms/aaddevsamplesv2) and [documentation](https://aka.ms/aaddevv2) to help you get started with learning the Microsoft identity platform. ## Community Help and Support We leverage Stack Overflow 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. We recommend you use the "msal" tag so we can see it! Here is the latest Q&A on Stack Overflow for MSAL: [http://stackoverflow.com/questions/tagged/msal](http://stackoverflow.com/questions/tagged/msal) ## Security Reporting 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](http://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/security/dd252948) and subscribing to Security Advisory Alerts. ## Contributing 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. ## We Value and Adhere to the Microsoft Open Source Code of Conduct 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. microsoft-authentication-library-for-python-1.1.0/contributing.md000066400000000000000000000075241361242362500253450ustar00rootroot00000000000000# CONTRIBUTING Azure Active Directory SDK projects welcomes new contributors. This document will guide you through the process. ### CONTRIBUTOR LICENSE AGREEMENT Please visit [https://cla.microsoft.com/](https://cla.microsoft.com/) and sign the Contributor License Agreement. You only need to do that once. We can not look at your code until you've submitted this request. ### FORK Fork this project on GitHub and check out your copy. Example for Project Foo (which can be any ADAL or MSAL or just any library): ``` $ git clone git@github.com:username/project-foo.git $ cd project-foo $ git remote add upstream git@github.com:AzureAD/project-foo.git ``` No need to decide if you want your feature or bug fix to go into the dev branch or the master branch. **All bug fixes and new features should go into the dev branch.** The master branch is effectively frozen; patches that change the SDKs protocols or API surface area or affect the run-time behavior of the SDK will be rejected. 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 projects. Do not send your patch to us, we cannot accept it. In case of doubt, open an issue in the [issue tracker](issues). Especially do so if you plan to work on a major change in functionality. Nothing is more frustrating than seeing your hard work go to waste because your vision does not align with our goals for the SDK. ### BRANCH Okay, so you have decided on the proper branch. Create a feature branch and start hacking: ``` $ git checkout -b my-feature-branch ``` ### COMMIT Make sure git knows your name and email address: ``` $ git config --global user.name "J. Random User" $ git config --global user.email "j.random.user@example.com" ``` Writing good commit logs is important. A commit log should describe what changed and why. Follow these guidelines when writing one: 1. The first line should be 50 characters or less and contain a short description of the change prefixed with the name of the changed subsystem (e.g. "net: add localAddress and localPort to Socket"). 2. Keep the second line blank. 3. Wrap all other lines at 72 columns. A good commit log looks like this: ``` fix: explaining the commit in one line Body of commit message is a few lines of text, explaining things in more detail, possibly giving some background about the issue being fixed, etc etc. The body of the commit message can be several paragraphs, and please do proper word-wrap and keep columns shorter than about 72 characters or so. That way `git log` will show things nicely even when it is indented. ``` The header line should be meaningful; it is what other people see when they run `git shortlog` or `git log --oneline`. Check the output of `git log --oneline files_that_you_changed` to find out what directories your changes touch. ### REBASE Use `git rebase` (not `git merge`) to sync your work from time to time. ``` $ git fetch upstream $ git rebase upstream/v0.1 # or upstream/master ``` ### TEST Bug fixes and features should come with tests. Add your tests in the 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 structured (license boilerplate, common includes, etc.). Make sure that all tests pass. ### PUSH ``` $ git push origin my-feature-branch ``` Go to https://github.com/username/microsoft-authentication-library-for-***.git and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. If there are comments to address, apply your changes in a separate commit and push that to your feature branch. Post a comment in the pull request afterwards; GitHub does not send out notifications when you add commits. microsoft-authentication-library-for-python-1.1.0/docs/000077500000000000000000000000001361242362500232345ustar00rootroot00000000000000microsoft-authentication-library-for-python-1.1.0/docs/Makefile000066400000000000000000000011041361242362500246700ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)microsoft-authentication-library-for-python-1.1.0/docs/conf.py000066400000000000000000000123621361242362500245370ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = u'MSAL Python' copyright = u'2018, Microsoft' author = u'Microsoft' # The short X.Y version from msal import __version__ as version # The full version, including alpha/beta/rc tags release = version # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.githubpages', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # # html_theme = 'alabaster' html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'MSALPythondoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'MSALPython.tex', u'MSAL Python Documentation', u'Microsoft', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'msalpython', u'MSAL Python Documentation', [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'MSALPython', u'MSAL Python Documentation', author, 'MSALPython', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- microsoft-authentication-library-for-python-1.1.0/docs/index.rst000066400000000000000000000041711361242362500251000ustar00rootroot00000000000000.. MSAL Python documentation master file, created by sphinx-quickstart on Tue Dec 18 10:53:22 2018. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. .. This file is also inspired by https://pythonhosted.org/an_example_pypi_project/sphinx.html#full-code-example Welcome to MSAL Python's documentation! ======================================= .. toctree:: :maxdepth: 2 :caption: Contents: You can find high level conceptual documentations in the project `README `_ and `workable samples inside the project code base `_ . The documentation hosted here is for API Reference. PublicClientApplication and ConfidentialClientApplication ========================================================= MSAL proposes a clean separation between `public client applications and confidential client applications `_. They are implemented as two separated classes, with different methods for different authentication scenarios. PublicClientApplication ----------------------- .. autoclass:: msal.PublicClientApplication :members: ConfidentialClientApplication ----------------------------- .. autoclass:: msal.ConfidentialClientApplication :members: Shared Methods -------------- Both PublicClientApplication and ConfidentialClientApplication have following methods inherited from their base class. You typically do not need to initiate this base class, though. .. autoclass:: msal.ClientApplication :members: .. automethod:: __init__ TokenCache ========== One of the parameter accepted by both `PublicClientApplication` and `ConfidentialClientApplication` is the `TokenCache`. .. autoclass:: msal.TokenCache :members: You can subclass it to add new behavior, such as, token serialization. See `SerializableTokenCache` for example. .. autoclass:: msal.SerializableTokenCache :members: Indices and tables ================== * :ref:`genindex` * :ref:`search` microsoft-authentication-library-for-python-1.1.0/docs/make.bat000066400000000000000000000014231361242362500246410ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd microsoft-authentication-library-for-python-1.1.0/msal/000077500000000000000000000000001361242362500232405ustar00rootroot00000000000000microsoft-authentication-library-for-python-1.1.0/msal/__init__.py000066400000000000000000000027741361242362500253630ustar00rootroot00000000000000#------------------------------------------------------------------------------ # # Copyright (c) Microsoft Corporation. # All rights reserved. # # This code is licensed under the MIT License. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files(the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions : # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #------------------------------------------------------------------------------ from .application import ( __version__, ClientApplication, ConfidentialClientApplication, PublicClientApplication, ) from .token_cache import TokenCache, SerializableTokenCache microsoft-authentication-library-for-python-1.1.0/msal/application.py000066400000000000000000001223151361242362500261210ustar00rootroot00000000000000import time try: # Python 2 from urlparse import urljoin except: # Python 3 from urllib.parse import urljoin import logging import sys import warnings import uuid import requests from .oauth2cli import Client, JwtAssertionCreator from .authority import Authority from .mex import send_request as mex_send_request from .wstrust_request import send_request as wst_send_request from .wstrust_response import * from .token_cache import TokenCache # The __init__.py will import this. Not the other way around. __version__ = "1.1.0" logger = logging.getLogger(__name__) def decorate_scope( scopes, client_id, reserved_scope=frozenset(['openid', 'profile', 'offline_access'])): if not isinstance(scopes, (list, set, tuple)): raise ValueError("The input scopes should be a list, tuple, or set") scope_set = set(scopes) # Input scopes is typically a list. Copy it to a set. if scope_set & reserved_scope: # These scopes are reserved for the API to provide good experience. # We could make the developer pass these and then if they do they will # come back asking why they don't see refresh token or user information. raise ValueError( "API does not accept {} value as user-provided scopes".format( reserved_scope)) if client_id in scope_set: if len(scope_set) > 1: # We make developers pass their client id, so that they can express # the intent that they want the token for themselves (their own # app). # If we do not restrict them to passing only client id then they # could write code where they expect an id token but end up getting # access_token. raise ValueError("Client Id can only be provided as a single scope") decorated = set(reserved_scope) # Make a writable copy else: decorated = scope_set | reserved_scope return list(decorated) CLIENT_REQUEST_ID = 'client-request-id' CLIENT_CURRENT_TELEMETRY = 'x-client-current-telemetry' def _get_new_correlation_id(): return str(uuid.uuid4()) def _build_current_telemetry_request_header(public_api_id, force_refresh=False): return "1|{},{}|".format(public_api_id, "1" if force_refresh else "0") def extract_certs(public_cert_content): # Parses raw public certificate file contents and returns a list of strings # Usage: headers = {"x5c": extract_certs(open("my_cert.pem").read())} public_certificates = re.findall( r'-----BEGIN CERTIFICATE-----(?P[^-]+)-----END CERTIFICATE-----', public_cert_content, re.I) if public_certificates: return [cert.strip() for cert in public_certificates] # The public cert tags are not found in the input, # let's make best effort to exclude a private key pem file. if "PRIVATE KEY" in public_cert_content: raise ValueError( "We expect your public key but detect a private key instead") return [public_cert_content.strip()] class ClientApplication(object): ACQUIRE_TOKEN_SILENT_ID = "84" ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID = "301" ACQUIRE_TOKEN_ON_BEHALF_OF_ID = "523" ACQUIRE_TOKEN_BY_DEVICE_FLOW_ID = "622" ACQUIRE_TOKEN_FOR_CLIENT_ID = "730" ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID = "832" GET_ACCOUNTS_ID = "902" REMOVE_ACCOUNT_ID = "903" def __init__( self, client_id, client_credential=None, authority=None, validate_authority=True, token_cache=None, verify=True, proxies=None, timeout=None, client_claims=None, app_name=None, app_version=None): """Create an instance of application. :param client_id: Your app has a client_id after you register it on AAD. :param client_credential: For :class:`PublicClientApplication`, you simply use `None` here. For :class:`ConfidentialClientApplication`, it can be a string containing client secret, or an X509 certificate container in this form:: { "private_key": "...-----BEGIN PRIVATE KEY-----...", "thumbprint": "A1B2C3D4E5F6...", "public_certificate": "...-----BEGIN CERTIFICATE-----..." (Optional. See below.) } *Added in version 0.5.0*: public_certificate (optional) is public key certificate which will be sent through 'x5c' JWT header only for subject name and issuer authentication to support cert auto rolls. :param dict client_claims: *Added in version 0.5.0*: It is a dictionary of extra claims that would be signed by by this :class:`ConfidentialClientApplication` 's private key. For example, you can use {"client_ip": "x.x.x.x"}. You may also override any of the following default claims:: { "aud": the_token_endpoint, "iss": self.client_id, "sub": same_as_issuer, "exp": now + 10_min, "iat": now, "jti": a_random_uuid } :param str authority: A URL that identifies a token authority. It should be of the format https://login.microsoftonline.com/your_tenant By default, we will use https://login.microsoftonline.com/common :param bool validate_authority: (optional) Turns authority validation on or off. This parameter default to true. :param TokenCache cache: Sets the token cache used by this ClientApplication instance. By default, an in-memory cache will be created and used. :param verify: (optional) It will be passed to the `verify parameter in the underlying requests library `_ :param proxies: (optional) It will be passed to the `proxies parameter in the underlying requests library `_ :param timeout: (optional) It will be passed to the `timeout parameter in the underlying requests library `_ :param app_name: (optional) You can provide your application name for Microsoft telemetry purposes. Default value is None, means it will not be passed to Microsoft. :param app_version: (optional) You can provide your application version for Microsoft telemetry purposes. Default value is None, means it will not be passed to Microsoft. """ self.client_id = client_id self.client_credential = client_credential self.client_claims = client_claims self.verify = verify self.proxies = proxies self.timeout = timeout self.app_name = app_name self.app_version = app_version self.authority = Authority( authority or "https://login.microsoftonline.com/common/", validate_authority, verify=verify, proxies=proxies, timeout=timeout) # Here the self.authority is not the same type as authority in input self.token_cache = token_cache or TokenCache() self.client = self._build_client(client_credential, self.authority) self.authority_groups = None def _build_client(self, client_credential, authority): client_assertion = None client_assertion_type = None default_headers = { "x-client-sku": "MSAL.Python", "x-client-ver": __version__, "x-client-os": sys.platform, "x-client-cpu": "x64" if sys.maxsize > 2 ** 32 else "x86", } if self.app_name: default_headers['x-app-name'] = self.app_name if self.app_version: default_headers['x-app-ver'] = self.app_version default_body = {"client_info": 1} if isinstance(client_credential, dict): assert ("private_key" in client_credential and "thumbprint" in client_credential) headers = {} if 'public_certificate' in client_credential: headers["x5c"] = extract_certs(client_credential['public_certificate']) assertion = JwtAssertionCreator( client_credential["private_key"], algorithm="RS256", sha1_thumbprint=client_credential.get("thumbprint"), headers=headers) client_assertion = assertion.create_regenerative_assertion( audience=authority.token_endpoint, issuer=self.client_id, additional_claims=self.client_claims or {}) client_assertion_type = Client.CLIENT_ASSERTION_TYPE_JWT else: default_body['client_secret'] = client_credential server_configuration = { "authorization_endpoint": authority.authorization_endpoint, "token_endpoint": authority.token_endpoint, "device_authorization_endpoint": urljoin(authority.token_endpoint, "devicecode"), } return Client( server_configuration, self.client_id, default_headers=default_headers, default_body=default_body, client_assertion=client_assertion, client_assertion_type=client_assertion_type, on_obtaining_tokens=self.token_cache.add, on_removing_rt=self.token_cache.remove_rt, on_updating_rt=self.token_cache.update_rt, verify=self.verify, proxies=self.proxies, timeout=self.timeout) def get_authorization_request_url( self, scopes, # type: list[str] # additional_scope=None, # type: Optional[list] login_hint=None, # type: Optional[str] state=None, # Recommended by OAuth2 for CSRF protection redirect_uri=None, response_type="code", # Can be "token" if you use Implicit Grant prompt=None, **kwargs): """Constructs a URL for you to start a Authorization Code Grant. :param list[str] scopes: (Required) Scopes requested to access a protected API (a resource). :param str state: Recommended by OAuth2 for CSRF protection. :param str login_hint: Identifier of the user. Generally a User Principal Name (UPN). :param str redirect_uri: Address to return to upon receiving a response from the authority. :param str response_type: Default value is "code" for an OAuth2 Authorization Code grant. You can use other content such as "id_token". :param str prompt: By default, no prompt value will be sent, not even "none". You will have to specify a value explicitly. Its valid values are defined in Open ID Connect specs https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest :return: The authorization url as a string. """ """ # TBD: this would only be meaningful in a new acquire_token_interactive() :param additional_scope: Additional scope is a concept only in AAD. It refers to other resources you might want to prompt to consent for in the same interaction, but for which you won't get back a token for in this particular operation. (Under the hood, we simply merge scope and additional_scope before sending them on the wire.) """ authority = kwargs.pop("authority", None) # Historically we support this if authority: warnings.warn( "We haven't decided if this method will accept authority parameter") # The previous implementation is, it will use self.authority by default. # Multi-tenant app can use new authority on demand the_authority = Authority( authority, verify=self.verify, proxies=self.proxies, timeout=self.timeout, ) if authority else self.authority client = Client( {"authorization_endpoint": the_authority.authorization_endpoint}, self.client_id) return client.build_auth_request_uri( response_type=response_type, redirect_uri=redirect_uri, state=state, login_hint=login_hint, prompt=prompt, scope=decorate_scope(scopes, self.client_id), ) def acquire_token_by_authorization_code( self, code, scopes, # Syntactically required. STS accepts empty value though. redirect_uri=None, # REQUIRED, if the "redirect_uri" parameter was included in the # authorization request as described in Section 4.1.1, and their # values MUST be identical. **kwargs): """The second half of the Authorization Code Grant. :param code: The authorization code returned from Authorization Server. :param list[str] scopes: (Required) Scopes requested to access a protected API (a resource). If you requested user consent for multiple resources, here you will typically want to provide a subset of what you required in AuthCode. OAuth2 was designed mostly for singleton services, where tokens are always meant for the same resource and the only changes are in the scopes. In AAD, tokens can be issued for multiple 3rd party resources. You can ask authorization code for multiple resources, but when you redeem it, the token is for only one intended recipient, called audience. So the developer need to specify a scope so that we can restrict the token to be issued for the corresponding audience. :return: A dict representing the json response from AAD: - A successful response would contain "access_token" key, - an error response would contain "error" and usually "error_description". """ # If scope is absent on the wire, STS will give you a token associated # to the FIRST scope sent during the authorization request. # So in theory, you can omit scope here when you were working with only # one scope. But, MSAL decorates your scope anyway, so they are never # really empty. assert isinstance(scopes, list), "Invalid parameter type" self._validate_ssh_cert_input_data(kwargs.get("data", {})) return self.client.obtain_token_by_authorization_code( code, redirect_uri=redirect_uri, scope=decorate_scope(scopes, self.client_id), headers={ CLIENT_REQUEST_ID: _get_new_correlation_id(), CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID), }, **kwargs) def get_accounts(self, username=None): """Get a list of accounts which previously signed in, i.e. exists in cache. An account can later be used in :func:`~acquire_token_silent` to find its tokens. :param username: Filter accounts with this username only. Case insensitive. :return: A list of account objects. Each account is a dict. For now, we only document its "username" field. Your app can choose to display those information to end user, and allow user to choose one of his/her accounts to proceed. """ accounts = self._find_msal_accounts(environment=self.authority.instance) if not accounts: # Now try other aliases of this authority instance for alias in self._get_authority_aliases(self.authority.instance): accounts = self._find_msal_accounts(environment=alias) if accounts: break if username: # Federated account["username"] from AAD could contain mixed case lowercase_username = username.lower() accounts = [a for a in accounts if a["username"].lower() == lowercase_username] # Does not further filter by existing RTs here. It probably won't matter. # Because in most cases Accounts and RTs co-exist. # Even in the rare case when an RT is revoked and then removed, # acquire_token_silent() would then yield no result, # apps would fall back to other acquire methods. This is the standard pattern. return accounts def _find_msal_accounts(self, environment): return [a for a in self.token_cache.find( TokenCache.CredentialType.ACCOUNT, query={"environment": environment}) if a["authority_type"] in ( TokenCache.AuthorityType.ADFS, TokenCache.AuthorityType.MSSTS)] def _get_authority_aliases(self, instance): if not self.authority_groups: resp = requests.get( "https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize", headers={'Accept': 'application/json'}, verify=self.verify, proxies=self.proxies, timeout=self.timeout) resp.raise_for_status() self.authority_groups = [ set(group['aliases']) for group in resp.json()['metadata']] for group in self.authority_groups: if instance in group: return [alias for alias in group if alias != instance] return [] def remove_account(self, account): """Sign me out and forget me from token cache""" self._forget_me(account) def _sign_out(self, home_account): # Remove all relevant RTs and ATs from token cache owned_by_home_account = { "environment": home_account["environment"], "home_account_id": home_account["home_account_id"],} # realm-independent app_metadata = self._get_app_metadata(home_account["environment"]) # Remove RTs/FRTs, and they are realm-independent for rt in [rt for rt in self.token_cache.find( TokenCache.CredentialType.REFRESH_TOKEN, query=owned_by_home_account) # Do RT's app ownership check as a precaution, in case family apps # and 3rd-party apps share same token cache, although they should not. if rt["client_id"] == self.client_id or ( app_metadata.get("family_id") # Now let's settle family business and rt.get("family_id") == app_metadata["family_id"]) ]: self.token_cache.remove_rt(rt) for at in self.token_cache.find( # Remove ATs # Regardless of realm, b/c we've removed realm-independent RTs anyway TokenCache.CredentialType.ACCESS_TOKEN, query=owned_by_home_account): # To avoid the complexity of locating sibling family app's AT, # we skip AT's app ownership check. # It means ATs for other apps will also be removed, it is OK because: # * non-family apps are not supposed to share token cache to begin with; # * Even if it happens, we keep other app's RT already, so SSO still works self.token_cache.remove_at(at) def _forget_me(self, home_account): # It implies signout, and then also remove all relevant accounts and IDTs self._sign_out(home_account) owned_by_home_account = { "environment": home_account["environment"], "home_account_id": home_account["home_account_id"],} # realm-independent for idt in self.token_cache.find( # Remove IDTs, regardless of realm TokenCache.CredentialType.ID_TOKEN, query=owned_by_home_account): self.token_cache.remove_idt(idt) for a in self.token_cache.find( # Remove Accounts, regardless of realm TokenCache.CredentialType.ACCOUNT, query=owned_by_home_account): self.token_cache.remove_account(a) def acquire_token_silent( self, scopes, # type: List[str] account, # type: Optional[Account] authority=None, # See get_authorization_request_url() force_refresh=False, # type: Optional[boolean] **kwargs): """Acquire an access token for given account, without user interaction. It is done either by finding a valid access token from cache, or by finding a valid refresh token from cache and then automatically use it to redeem a new access token. This method will combine the cache empty and refresh error into one return value, `None`. If your app does not care about the exact token refresh error during token cache look-up, then this method is easier and recommended. Internally, this method calls :func:`~acquire_token_silent_with_error`. :return: - A dict containing no "error" key, and typically contains an "access_token" key, if cache lookup succeeded. - None when cache lookup does not yield a token. """ result = self.acquire_token_silent_with_error( scopes, account, authority, force_refresh, **kwargs) return result if result and "error" not in result else None def acquire_token_silent_with_error( self, scopes, # type: List[str] account, # type: Optional[Account] authority=None, # See get_authorization_request_url() force_refresh=False, # type: Optional[boolean] **kwargs): """Acquire an access token for given account, without user interaction. It is done either by finding a valid access token from cache, or by finding a valid refresh token from cache and then automatically use it to redeem a new access token. This method will differentiate cache empty from token refresh error. If your app cares the exact token refresh error during token cache look-up, then this method is suitable. Otherwise, the other method :func:`~acquire_token_silent` is recommended. :param list[str] scopes: (Required) Scopes requested to access a protected API (a resource). :param account: one of the account object returned by :func:`~get_accounts`, or use None when you want to find an access token for this client. :param force_refresh: If True, it will skip Access Token look-up, and try to find a Refresh Token to obtain a new Access Token. :return: - A dict containing no "error" key, and typically contains an "access_token" key, if cache lookup succeeded. - None when there is simply no token in the cache. - A dict containing an "error" key, when token refresh failed. """ assert isinstance(scopes, list), "Invalid parameter type" self._validate_ssh_cert_input_data(kwargs.get("data", {})) correlation_id = _get_new_correlation_id() if authority: warnings.warn("We haven't decided how/if this method will accept authority parameter") # the_authority = Authority( # authority, # verify=self.verify, proxies=self.proxies, timeout=self.timeout, # ) if authority else self.authority result = self._acquire_token_silent_from_cache_and_possibly_refresh_it( scopes, account, self.authority, force_refresh=force_refresh, correlation_id=correlation_id, **kwargs) if result and "error" not in result: return result final_result = result for alias in self._get_authority_aliases(self.authority.instance): the_authority = Authority( "https://" + alias + "/" + self.authority.tenant, validate_authority=False, verify=self.verify, proxies=self.proxies, timeout=self.timeout) result = self._acquire_token_silent_from_cache_and_possibly_refresh_it( scopes, account, the_authority, force_refresh=force_refresh, correlation_id=correlation_id, **kwargs) if result: if "error" not in result: return result final_result = result if final_result and final_result.get("suberror"): final_result["classification"] = { # Suppress these suberrors, per #57 "bad_token": "", "token_expired": "", "protection_policy_required": "", "client_mismatch": "", "device_authentication_failed": "", }.get(final_result["suberror"], final_result["suberror"]) return final_result def _acquire_token_silent_from_cache_and_possibly_refresh_it( self, scopes, # type: List[str] account, # type: Optional[Account] authority, # This can be different than self.authority force_refresh=False, # type: Optional[boolean] **kwargs): if not force_refresh: query={ "client_id": self.client_id, "environment": authority.instance, "realm": authority.tenant, "home_account_id": (account or {}).get("home_account_id"), } key_id = kwargs.get("data", {}).get("key_id") if key_id: # Some token types (SSH-certs, POP) are bound to a key query["key_id"] = key_id matches = self.token_cache.find( self.token_cache.CredentialType.ACCESS_TOKEN, target=scopes, query=query) now = time.time() for entry in matches: expires_in = int(entry["expires_on"]) - now if expires_in < 5*60: continue # Removal is not necessary, it will be overwritten logger.debug("Cache hit an AT") return { # Mimic a real response "access_token": entry["secret"], "token_type": entry.get("token_type", "Bearer"), "expires_in": int(expires_in), # OAuth2 specs defines it as int } return self._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family( authority, decorate_scope(scopes, self.client_id), account, force_refresh=force_refresh, **kwargs) def _acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family( self, authority, scopes, account, **kwargs): query = { "environment": authority.instance, "home_account_id": (account or {}).get("home_account_id"), # "realm": authority.tenant, # AAD RTs are tenant-independent } app_metadata = self._get_app_metadata(authority.instance) if not app_metadata: # Meaning this app is now used for the first time. # When/if we have a way to directly detect current app's family, # we'll rewrite this block, to support multiple families. # For now, we try existing RTs (*). If it works, we are in that family. # (*) RTs of a different app/family are not supposed to be # shared with or accessible by us in the first place. at = self._acquire_token_silent_by_finding_specific_refresh_token( authority, scopes, dict(query, family_id="1"), # A hack, we have only 1 family for now rt_remover=lambda rt_item: None, # NO-OP b/c RTs are likely not mine break_condition=lambda response: # Break loop when app not in family # Based on an AAD-only behavior mentioned in internal doc here # https://msazure.visualstudio.com/One/_git/ESTS-Docs/pullrequest/1138595 "client_mismatch" in response.get("error_additional_info", []), **kwargs) if at and "error" not in at: return at if app_metadata.get("family_id"): # Meaning this app belongs to this family at = self._acquire_token_silent_by_finding_specific_refresh_token( authority, scopes, dict(query, family_id=app_metadata["family_id"]), **kwargs) if at and "error" not in at: return at # Either this app is an orphan, so we will naturally use its own RT; # or all attempts above have failed, so we fall back to non-foci behavior. return self._acquire_token_silent_by_finding_specific_refresh_token( authority, scopes, dict(query, client_id=self.client_id), **kwargs) def _get_app_metadata(self, environment): apps = self.token_cache.find( # Use find(), rather than token_cache.get(...) TokenCache.CredentialType.APP_METADATA, query={ "environment": environment, "client_id": self.client_id}) return apps[0] if apps else {} def _acquire_token_silent_by_finding_specific_refresh_token( self, authority, scopes, query, rt_remover=None, break_condition=lambda response: False, force_refresh=False, correlation_id=None, **kwargs): matches = self.token_cache.find( self.token_cache.CredentialType.REFRESH_TOKEN, # target=scopes, # AAD RTs are scope-independent query=query) logger.debug("Found %d RTs matching %s", len(matches), query) client = self._build_client(self.client_credential, authority) response = None # A distinguishable value to mean cache is empty for entry in matches: logger.debug("Cache attempts an RT") response = client.obtain_token_by_refresh_token( entry, rt_getter=lambda token_item: token_item["secret"], on_removing_rt=rt_remover or self.token_cache.remove_rt, scope=scopes, headers={ CLIENT_REQUEST_ID: correlation_id or _get_new_correlation_id(), CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( self.ACQUIRE_TOKEN_SILENT_ID, force_refresh=force_refresh), }, **kwargs) if "error" not in response: return response logger.debug("Refresh failed. {error}: {error_description}".format( error=response.get("error"), error_description=response.get("error_description"), )) if break_condition(response): break return response # Returns the latest error (if any), or just None def _validate_ssh_cert_input_data(self, data): if data.get("token_type") == "ssh-cert": if not data.get("req_cnf"): raise ValueError( "When requesting an SSH certificate, " "you must include a string parameter named 'req_cnf' " "containing the public key in JWK format " "(https://tools.ietf.org/html/rfc7517).") if not data.get("key_id"): raise ValueError( "When requesting an SSH certificate, " "you must include a string parameter named 'key_id' " "which identifies the key in the 'req_cnf' argument.") class PublicClientApplication(ClientApplication): # browser app or mobile app DEVICE_FLOW_CORRELATION_ID = "_correlation_id" def __init__(self, client_id, client_credential=None, **kwargs): if client_credential is not None: raise ValueError("Public Client should not possess credentials") super(PublicClientApplication, self).__init__( client_id, client_credential=None, **kwargs) def initiate_device_flow(self, scopes=None, **kwargs): """Initiate a Device Flow instance, which will be used in :func:`~acquire_token_by_device_flow`. :param list[str] scopes: Scopes requested to access a protected API (a resource). :return: A dict representing a newly created Device Flow object. - A successful response would contain "user_code" key, among others - an error response would contain some other readable key/value pairs. """ correlation_id = _get_new_correlation_id() flow = self.client.initiate_device_flow( scope=decorate_scope(scopes or [], self.client_id), headers={ CLIENT_REQUEST_ID: correlation_id, # CLIENT_CURRENT_TELEMETRY is not currently required }, **kwargs) flow[self.DEVICE_FLOW_CORRELATION_ID] = correlation_id return flow def acquire_token_by_device_flow(self, flow, **kwargs): """Obtain token by a device flow object, with customizable polling effect. :param dict flow: A dict previously generated by :func:`~initiate_device_flow`. By default, this method's polling effect will block current thread. You can abort the polling loop at any time, by changing the value of the flow's "expires_at" key to 0. :return: A dict representing the json response from AAD: - A successful response would contain "access_token" key, - an error response would contain "error" and usually "error_description". """ return self.client.obtain_token_by_device_flow( flow, data=dict(kwargs.pop("data", {}), code=flow["device_code"]), # 2018-10-4 Hack: # during transition period, # service seemingly need both device_code and code parameter. headers={ CLIENT_REQUEST_ID: flow.get(self.DEVICE_FLOW_CORRELATION_ID) or _get_new_correlation_id(), CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( self.ACQUIRE_TOKEN_BY_DEVICE_FLOW_ID), }, **kwargs) def acquire_token_by_username_password( self, username, password, scopes, **kwargs): """Gets a token for a given resource via user credentails. See this page for constraints of Username Password Flow. https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Username-Password-Authentication :param str username: Typically a UPN in the form of an email address. :param str password: The password. :param list[str] scopes: Scopes requested to access a protected API (a resource). :return: A dict representing the json response from AAD: - A successful response would contain "access_token" key, - an error response would contain "error" and usually "error_description". """ scopes = decorate_scope(scopes, self.client_id) headers = { CLIENT_REQUEST_ID: _get_new_correlation_id(), CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( self.ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID), } if not self.authority.is_adfs: user_realm_result = self.authority.user_realm_discovery( username, correlation_id=headers[CLIENT_REQUEST_ID]) if user_realm_result.get("account_type") == "Federated": return self._acquire_token_by_username_password_federated( user_realm_result, username, password, scopes=scopes, headers=headers, **kwargs) return self.client.obtain_token_by_username_password( username, password, scope=scopes, headers=headers, **kwargs) def _acquire_token_by_username_password_federated( self, user_realm_result, username, password, scopes=None, **kwargs): verify = kwargs.pop("verify", self.verify) proxies = kwargs.pop("proxies", self.proxies) wstrust_endpoint = {} if user_realm_result.get("federation_metadata_url"): wstrust_endpoint = mex_send_request( user_realm_result["federation_metadata_url"], verify=verify, proxies=proxies) if wstrust_endpoint is None: raise ValueError("Unable to find wstrust endpoint from MEX. " "This typically happens when attempting MSA accounts. " "More details available here. " "https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Username-Password-Authentication") logger.debug("wstrust_endpoint = %s", wstrust_endpoint) wstrust_result = wst_send_request( username, password, user_realm_result.get("cloud_audience_urn"), wstrust_endpoint.get("address", # Fallback to an AAD supplied endpoint user_realm_result.get("federation_active_auth_url")), wstrust_endpoint.get("action"), verify=verify, proxies=proxies) if not ("token" in wstrust_result and "type" in wstrust_result): raise RuntimeError("Unsuccessful RSTR. %s" % wstrust_result) GRANT_TYPE_SAML1_1 = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer' grant_type = { SAML_TOKEN_TYPE_V1: GRANT_TYPE_SAML1_1, SAML_TOKEN_TYPE_V2: self.client.GRANT_TYPE_SAML2, WSS_SAML_TOKEN_PROFILE_V1_1: GRANT_TYPE_SAML1_1, WSS_SAML_TOKEN_PROFILE_V2: self.client.GRANT_TYPE_SAML2 }.get(wstrust_result.get("type")) if not grant_type: raise RuntimeError( "RSTR returned unknown token type: %s", wstrust_result.get("type")) self.client.grant_assertion_encoders.setdefault( # Register a non-standard type grant_type, self.client.encode_saml_assertion) return self.client.obtain_token_by_assertion( wstrust_result["token"], grant_type, scope=scopes, **kwargs) class ConfidentialClientApplication(ClientApplication): # server-side web app def acquire_token_for_client(self, scopes, **kwargs): """Acquires token for the current confidential client, not for an end user. :param list[str] scopes: (Required) Scopes requested to access a protected API (a resource). :return: A dict representing the json response from AAD: - A successful response would contain "access_token" key, - an error response would contain "error" and usually "error_description". """ # TBD: force_refresh behavior return self.client.obtain_token_for_client( scope=scopes, # This grant flow requires no scope decoration headers={ CLIENT_REQUEST_ID: _get_new_correlation_id(), CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( self.ACQUIRE_TOKEN_FOR_CLIENT_ID), }, **kwargs) def acquire_token_on_behalf_of(self, user_assertion, scopes, **kwargs): """Acquires token using on-behalf-of (OBO) flow. The current app is a middle-tier service which was called with a token representing an end user. The current app can use such token (a.k.a. a user assertion) to request another token to access downstream web API, on behalf of that user. See `detail docs here `_ . The current middle-tier app has no user interaction to obtain consent. See how to gain consent upfront for your middle-tier app from this article. https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#gaining-consent-for-the-middle-tier-application :param str user_assertion: The incoming token already received by this app :param list[str] scopes: Scopes required by downstream API (a resource). :return: A dict representing the json response from AAD: - A successful response would contain "access_token" key, - an error response would contain "error" and usually "error_description". """ # The implementation is NOT based on Token Exchange # https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16 return self.client.obtain_token_by_assertion( # bases on assertion RFC 7521 user_assertion, self.client.GRANT_TYPE_JWT, # IDTs and AAD ATs are all JWTs scope=decorate_scope(scopes, self.client_id), # Decoration is used for: # 1. Explicitly requesting an RT, without relying on AAD default # behavior, even though it currently still issues an RT. # 2. Requesting an IDT (which would otherwise be unavailable) # so that the calling app could use id_token_claims to implement # their own cache mapping, which is likely needed in web apps. data=dict(kwargs.pop("data", {}), requested_token_use="on_behalf_of"), headers={ CLIENT_REQUEST_ID: _get_new_correlation_id(), CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( self.ACQUIRE_TOKEN_ON_BEHALF_OF_ID), }, **kwargs) microsoft-authentication-library-for-python-1.1.0/msal/authority.py000066400000000000000000000140651361242362500256500ustar00rootroot00000000000000try: from urllib.parse import urlparse except ImportError: # Fall back to Python 2 from urlparse import urlparse import logging import requests from .exceptions import MsalServiceError logger = logging.getLogger(__name__) WORLD_WIDE = 'login.microsoftonline.com' # There was an alias login.windows.net WELL_KNOWN_AUTHORITY_HOSTS = set([ WORLD_WIDE, 'login.chinacloudapi.cn', 'login-us.microsoftonline.com', 'login.microsoftonline.us', 'login.microsoftonline.de', ]) WELL_KNOWN_B2C_HOSTS = [ "b2clogin.com", "b2clogin.cn", "b2clogin.us", "b2clogin.de", ] class Authority(object): """This class represents an (already-validated) authority. Once constructed, it contains members named "*_endpoint" for this instance. TODO: It will also cache the previously-validated authority instances. """ _domains_without_user_realm_discovery = set([]) def __init__(self, authority_url, validate_authority=True, verify=True, proxies=None, timeout=None, ): """Creates an authority instance, and also validates it. :param validate_authority: The Authority validation process actually checks two parts: instance (a.k.a. host) and tenant. We always do a tenant discovery. This parameter only controls whether an instance discovery will be performed. """ self.verify = verify self.proxies = proxies self.timeout = timeout authority, self.instance, tenant = canonicalize(authority_url) parts = authority.path.split('/') is_b2c = any(self.instance.endswith("." + d) for d in WELL_KNOWN_B2C_HOSTS) or ( len(parts) == 3 and parts[2].lower().startswith("b2c_")) if (tenant != "adfs" and (not is_b2c) and validate_authority and self.instance not in WELL_KNOWN_AUTHORITY_HOSTS): payload = instance_discovery( "https://{}{}/oauth2/v2.0/authorize".format( self.instance, authority.path), verify=verify, proxies=proxies, timeout=timeout) if payload.get("error") == "invalid_instance": raise ValueError( "invalid_instance: " "The authority you provided, %s, is not whitelisted. " "If it is indeed your legit customized domain name, " "you can turn off this check by passing in " "validate_authority=False" % authority_url) tenant_discovery_endpoint = payload['tenant_discovery_endpoint'] else: tenant_discovery_endpoint = ( 'https://{}{}{}/.well-known/openid-configuration'.format( self.instance, authority.path, # In B2C scenario, it is "/tenant/policy" "" if tenant == "adfs" else "/v2.0" # the AAD v2 endpoint )) openid_config = tenant_discovery( tenant_discovery_endpoint, verify=verify, proxies=proxies, timeout=timeout) logger.debug("openid_config = %s", openid_config) self.authorization_endpoint = openid_config['authorization_endpoint'] self.token_endpoint = openid_config['token_endpoint'] _, _, self.tenant = canonicalize(self.token_endpoint) # Usually a GUID self.is_adfs = self.tenant.lower() == 'adfs' def user_realm_discovery(self, username, correlation_id=None, response=None): # It will typically return a dict containing "ver", "account_type", # "federation_protocol", "cloud_audience_urn", # "federation_metadata_url", "federation_active_auth_url", etc. if self.instance not in self.__class__._domains_without_user_realm_discovery: resp = response or requests.get( "https://{netloc}/common/userrealm/{username}?api-version=1.0".format( netloc=self.instance, username=username), headers={'Accept':'application/json', 'client-request-id': correlation_id}, verify=self.verify, proxies=self.proxies, timeout=self.timeout) if resp.status_code != 404: resp.raise_for_status() return resp.json() self.__class__._domains_without_user_realm_discovery.add(self.instance) return {} # This can guide the caller to fall back normal ROPC flow def canonicalize(authority_url): # Returns (url_parsed_result, hostname_in_lowercase, tenant) authority = urlparse(authority_url) parts = authority.path.split("/") if authority.scheme != "https" or len(parts) < 2 or not parts[1]: raise ValueError( "Your given address (%s) should consist of " "an https url with a minimum of one segment in a path: e.g. " "https://login.microsoftonline.com/ " "or https://.b2clogin.com/.onmicrosoft.com/policy" % authority_url) return authority, authority.hostname, parts[1] def instance_discovery(url, **kwargs): return requests.get( # Note: This URL seemingly returns V1 endpoint only 'https://{}/common/discovery/instance'.format( WORLD_WIDE # Historically using WORLD_WIDE. Could use self.instance too # See https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/4.0.0/src/Microsoft.Identity.Client/Instance/AadInstanceDiscovery.cs#L101-L103 # and https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/4.0.0/src/Microsoft.Identity.Client/Instance/AadAuthority.cs#L19-L33 ), params={'authorization_endpoint': url, 'api-version': '1.0'}, **kwargs).json() def tenant_discovery(tenant_discovery_endpoint, **kwargs): # Returns Openid Configuration resp = requests.get(tenant_discovery_endpoint, **kwargs) payload = resp.json() if 'authorization_endpoint' in payload and 'token_endpoint' in payload: return payload raise MsalServiceError(status_code=resp.status_code, **payload) microsoft-authentication-library-for-python-1.1.0/msal/exceptions.py000066400000000000000000000032461361242362500260000ustar00rootroot00000000000000#------------------------------------------------------------------------------ # # Copyright (c) Microsoft Corporation. # All rights reserved. # # This code is licensed under the MIT License. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files(the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions : # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #------------------------------------------------------------------------------ class MsalError(Exception): # Define the template in Unicode to accommodate possible Unicode variables msg = u'An unspecified error' def __init__(self, *args, **kwargs): super(MsalError, self).__init__(self.msg.format(**kwargs), *args) self.kwargs = kwargs class MsalServiceError(MsalError): msg = u"{error}: {error_description}" microsoft-authentication-library-for-python-1.1.0/msal/mex.py000066400000000000000000000141771361242362500244150ustar00rootroot00000000000000#------------------------------------------------------------------------------ # # Copyright (c) Microsoft Corporation. # All rights reserved. # # This code is licensed under the MIT License. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files(the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions : # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #------------------------------------------------------------------------------ try: from urllib.parse import urlparse except: from urlparse import urlparse try: from xml.etree import cElementTree as ET except ImportError: from xml.etree import ElementTree as ET import requests def _xpath_of_root(route_to_leaf): # Construct an xpath suitable to find a root node which has a specified leaf return '/'.join(route_to_leaf + ['..'] * (len(route_to_leaf)-1)) def send_request(mex_endpoint, **kwargs): mex_document = requests.get( mex_endpoint, headers={'Content-Type': 'application/soap+xml'}, **kwargs).text return Mex(mex_document).get_wstrust_username_password_endpoint() class Mex(object): NS = { # Also used by wstrust_*.py 'wsdl': 'http://schemas.xmlsoap.org/wsdl/', 'sp': 'http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702', 'sp2005': 'http://schemas.xmlsoap.org/ws/2005/07/securitypolicy', 'wsu': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd', 'wsa': 'http://www.w3.org/2005/08/addressing', # Duplicate? 'wsa10': 'http://www.w3.org/2005/08/addressing', 'http': 'http://schemas.microsoft.com/ws/06/2004/policy/http', 'soap12': 'http://schemas.xmlsoap.org/wsdl/soap12/', 'wsp': 'http://schemas.xmlsoap.org/ws/2004/09/policy', 's': 'http://www.w3.org/2003/05/soap-envelope', 'wst': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512', 'trust': "http://docs.oasis-open.org/ws-sx/ws-trust/200512", # Duplicate? 'saml': "urn:oasis:names:tc:SAML:1.0:assertion", 'wst2005': 'http://schemas.xmlsoap.org/ws/2005/02/trust', # was named "t" } ACTION_13 = 'http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue' ACTION_2005 = 'http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue' def __init__(self, mex_document): self.dom = ET.fromstring(mex_document) def _get_policy_ids(self, components_to_leaf, binding_xpath): id_attr = '{%s}Id' % self.NS['wsu'] return set(["#{}".format(policy.get(id_attr)) for policy in self.dom.findall(_xpath_of_root(components_to_leaf), self.NS) # If we did not find any binding, this is potentially bad. if policy.find(binding_xpath, self.NS) is not None]) def _get_username_password_policy_ids(self): path = ['wsp:Policy', 'wsp:ExactlyOne', 'wsp:All', 'sp:SignedEncryptedSupportingTokens', 'wsp:Policy', 'sp:UsernameToken', 'wsp:Policy', 'sp:WssUsernameToken10'] policies = self._get_policy_ids(path, './/sp:TransportBinding') path2005 = ['wsp:Policy', 'wsp:ExactlyOne', 'wsp:All', 'sp2005:SignedSupportingTokens', 'wsp:Policy', 'sp2005:UsernameToken', 'wsp:Policy', 'sp2005:WssUsernameToken10'] policies.update(self._get_policy_ids(path2005, './/sp2005:TransportBinding')) return policies def _get_iwa_policy_ids(self): return self._get_policy_ids( ['wsp:Policy', 'wsp:ExactlyOne', 'wsp:All', 'http:NegotiateAuthentication'], './/sp2005:TransportBinding') def _get_bindings(self): bindings = {} # {binding_name: {"policy_uri": "...", "version": "..."}} for binding in self.dom.findall("wsdl:binding", self.NS): if (binding.find('soap12:binding', self.NS).get("transport") != 'http://schemas.xmlsoap.org/soap/http'): continue action = binding.find( 'wsdl:operation/soap12:operation', self.NS).get("soapAction") for pr in binding.findall("wsp:PolicyReference", self.NS): bindings[binding.get("name")] = { "policy_uri": pr.get("URI"), "action": action} return bindings def _get_endpoints(self, bindings, policy_ids): endpoints = [] for port in self.dom.findall('wsdl:service/wsdl:port', self.NS): binding_name = port.get("binding").split(':')[-1] # Should have 2 parts binding = bindings.get(binding_name) if binding and binding["policy_uri"] in policy_ids: address = port.find('wsa10:EndpointReference/wsa10:Address', self.NS) if address is not None and address.text.lower().startswith("https://"): endpoints.append( {"address": address.text, "action": binding["action"]}) return endpoints def get_wstrust_username_password_endpoint(self): """Returns {"address": "https://...", "action": "the soapAction value"}""" endpoints = self._get_endpoints( self._get_bindings(), self._get_username_password_policy_ids()) for e in endpoints: if e["action"] == self.ACTION_13: return e # Historically, we prefer ACTION_13 a.k.a. WsTrust13 return endpoints[0] if endpoints else None microsoft-authentication-library-for-python-1.1.0/msal/oauth2cli/000077500000000000000000000000001361242362500251325ustar00rootroot00000000000000microsoft-authentication-library-for-python-1.1.0/msal/oauth2cli/__init__.py000066400000000000000000000002461361242362500272450ustar00rootroot00000000000000__version__ = "0.3.0" from .oidc import Client from .assertion import JwtAssertionCreator from .assertion import JwtSigner # Obsolete. For backward compatibility. microsoft-authentication-library-for-python-1.1.0/msal/oauth2cli/assertion.py000066400000000000000000000110641361242362500275150ustar00rootroot00000000000000import time import binascii import base64 import uuid import logging import jwt logger = logging.getLogger(__name__) class AssertionCreator(object): def create_normal_assertion( self, audience, issuer, subject, expires_at=None, expires_in=600, issued_at=None, assertion_id=None, **kwargs): """Create an assertion in bytes, based on the provided claims. All parameter names are defined in https://tools.ietf.org/html/rfc7521#section-5 except the expires_in is defined here as lifetime-in-seconds, which will be automatically translated into expires_at in UTC. """ raise NotImplementedError("Will be implemented by sub-class") def create_regenerative_assertion( self, audience, issuer, subject=None, expires_in=600, **kwargs): """Create an assertion as a callable, which will then compute the assertion later when necessary. This is a useful optimization to reuse the client assertion. """ return AutoRefresher( # Returns a callable lambda a=audience, i=issuer, s=subject, e=expires_in, kwargs=kwargs: self.create_normal_assertion(a, i, s, expires_in=e, **kwargs), expires_in=max(expires_in-60, 0)) class AutoRefresher(object): """Cache the output of a factory, and auto-refresh it when necessary. Usage:: r = AutoRefresher(time.time, expires_in=5) for i in range(15): print(r()) # the timestamp change only after every 5 seconds time.sleep(1) """ def __init__(self, factory, expires_in=540): self._factory = factory self._expires_in = expires_in self._buf = {} def __call__(self): EXPIRES_AT, VALUE = "expires_at", "value" now = time.time() if self._buf.get(EXPIRES_AT, 0) <= now: logger.debug("Regenerating new assertion") self._buf = {VALUE: self._factory(), EXPIRES_AT: now + self._expires_in} else: logger.debug("Reusing still valid assertion") return self._buf.get(VALUE) class JwtAssertionCreator(AssertionCreator): def __init__(self, key, algorithm, sha1_thumbprint=None, headers=None): """Construct a Jwt assertion creator. Args: key (str): The key for signing, e.g. a base64 encoded private key. algorithm (str): "RS256", etc.. See https://pyjwt.readthedocs.io/en/latest/algorithms.html RSA and ECDSA algorithms require "pip install cryptography". sha1_thumbprint (str): The x5t aka X.509 certificate SHA-1 thumbprint. headers (dict): Additional headers, e.g. "kid" or "x5c" etc. """ self.key = key self.algorithm = algorithm self.headers = headers or {} if sha1_thumbprint: # https://tools.ietf.org/html/rfc7515#section-4.1.7 self.headers["x5t"] = base64.urlsafe_b64encode( binascii.a2b_hex(sha1_thumbprint)).decode() def create_normal_assertion( self, audience, issuer, subject=None, expires_at=None, expires_in=600, issued_at=None, assertion_id=None, not_before=None, additional_claims=None, **kwargs): """Create a JWT Assertion. Parameters are defined in https://tools.ietf.org/html/rfc7523#section-3 Key-value pairs in additional_claims will be added into payload as-is. """ now = time.time() payload = { 'aud': audience, 'iss': issuer, 'sub': subject or issuer, 'exp': expires_at or (now + expires_in), 'iat': issued_at or now, 'jti': assertion_id or str(uuid.uuid4()), } if not_before: payload['nbf'] = not_before payload.update(additional_claims or {}) try: return jwt.encode( payload, self.key, algorithm=self.algorithm, headers=self.headers) except: if self.algorithm.startswith("RS") or self.algorithm.starswith("ES"): logger.exception( 'Some algorithms requires "pip install cryptography". ' 'See https://pyjwt.readthedocs.io/en/latest/installation.html#cryptographic-dependencies-optional') raise # Obsolete. For backward compatibility. They will be removed in future versions. Signer = AssertionCreator # For backward compatibility JwtSigner = JwtAssertionCreator # For backward compatibility JwtSigner.sign_assertion = JwtAssertionCreator.create_normal_assertion # For backward compatibility microsoft-authentication-library-for-python-1.1.0/msal/oauth2cli/authcode.py000066400000000000000000000114251361242362500273030ustar00rootroot00000000000000# Note: This docstring is also used by this script's command line help. """A one-stop helper for desktop app to acquire an authorization code. It starts a web server to listen redirect_uri, waiting for auth code. It optionally opens a browser window to guide a human user to manually login. After obtaining an auth code, the web server will automatically shut down. """ import argparse import webbrowser import logging try: # Python 3 from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs, urlencode except ImportError: # Fall back to Python 2 from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from urlparse import urlparse, parse_qs from urllib import urlencode from .oauth2 import Client logger = logging.getLogger(__name__) def obtain_auth_code(listen_port, auth_uri=None): """This function will start a web server listening on http://localhost:port and then you need to open a browser on this device and visit your auth_uri. When interaction finishes, this function will return the auth code, and then shut down the local web server. :param listen_port: The local web server will listen at http://localhost: Unless the authorization server supports dynamic port, you need to use the same port when you register with your app. :param auth_uri: If provided, this function will try to open a local browser. :return: Hang indefinitely, until it receives and then return the auth code. """ exit_hint = "Visit http://localhost:{p}?code=exit to abort".format(p=listen_port) logger.warning(exit_hint) if auth_uri: page = "http://localhost:{p}?{q}".format(p=listen_port, q=urlencode({ "text": "Open this link to sign in. You may use incognito window", "link": auth_uri, "exit_hint": exit_hint, })) browse(page) server = HTTPServer(("", int(listen_port)), AuthCodeReceiver) try: server.authcode = None while not server.authcode: # Derived from # https://docs.python.org/2/library/basehttpserver.html#more-examples server.handle_request() return server.authcode finally: server.server_close() def browse(auth_uri): controller = webbrowser.get() # Get a default controller # Some Linux Distro does not setup default browser properly, # so we try to explicitly use some popular browser, if we found any. for browser in ["chrome", "firefox", "safari", "windows-default"]: try: controller = webbrowser.get(browser) break except webbrowser.Error: pass # This browser is not installed. Try next one. logger.info("Please open a browser on THIS device to visit: %s" % auth_uri) controller.open(auth_uri) class AuthCodeReceiver(BaseHTTPRequestHandler): def do_GET(self): # For flexibility, we choose to not check self.path matching redirect_uri #assert self.path.startswith('/THE_PATH_REGISTERED_BY_THE_APP') qs = parse_qs(urlparse(self.path).query) if qs.get('code'): # Then store it into the server instance ac = self.server.authcode = qs['code'][0] self._send_full_response('Authcode:\n{}'.format(ac)) # NOTE: Don't do self.server.shutdown() here. It'll halt the server. elif qs.get('text') and qs.get('link'): # Then display a landing page self._send_full_response( '{text}
{exit_hint}'.format( link=qs['link'][0], text=qs['text'][0], exit_hint=qs.get("exit_hint", [''])[0], )) else: self._send_full_response("This web service serves your redirect_uri") def _send_full_response(self, body, is_ok=True): self.send_response(200 if is_ok else 400) content_type = 'text/html' if body.startswith('<') else 'text/plain' self.send_header('Content-type', content_type) self.end_headers() self.wfile.write(body.encode("utf-8")) if __name__ == '__main__': logging.basicConfig(level=logging.INFO) p = parser = argparse.ArgumentParser( description=__doc__ + "The auth code received will be shown at stdout.") p.add_argument('endpoint', help="The auth endpoint for your app. For example: " "https://login.microsoftonline.com/your_tenant/oauth2/authorize") p.add_argument('client_id', help="The client_id of your application") p.add_argument('redirect_port', type=int, help="The port in redirect_uri") args = parser.parse_args() client = Client(args.client_id, authorization_endpoint=args.endpoint) auth_uri = client.build_auth_request_uri("code") print(obtain_auth_code(args.redirect_port, auth_uri)) microsoft-authentication-library-for-python-1.1.0/msal/oauth2cli/oauth2.py000066400000000000000000000556211361242362500267170ustar00rootroot00000000000000"""This OAuth2 client implementation aims to be spec-compliant, and generic.""" # OAuth2 spec https://tools.ietf.org/html/rfc6749 try: from urllib.parse import urlencode, parse_qs except ImportError: from urlparse import parse_qs from urllib import urlencode import logging import warnings import time import base64 import sys import requests string_types = (str,) if sys.version_info[0] >= 3 else (basestring, ) class BaseClient(object): # This low-level interface works. Yet you'll find its sub-class # more friendly to remind you what parameters are needed in each scenario. # More on Client Types at https://tools.ietf.org/html/rfc6749#section-2.1 @staticmethod def encode_saml_assertion(assertion): return base64.urlsafe_b64encode(assertion).rstrip(b'=') # Per RFC 7522 CLIENT_ASSERTION_TYPE_JWT = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" CLIENT_ASSERTION_TYPE_SAML2 = "urn:ietf:params:oauth:client-assertion-type:saml2-bearer" client_assertion_encoders = {CLIENT_ASSERTION_TYPE_SAML2: encode_saml_assertion} def __init__( self, server_configuration, # type: dict client_id, # type: str client_secret=None, # type: Optional[str] client_assertion=None, # type: Union[bytes, callable, None] client_assertion_type=None, # type: Optional[str] default_headers=None, # type: Optional[dict] default_body=None, # type: Optional[dict] verify=True, # type: Union[str, True, False, None] proxies=None, # type: Optional[dict] timeout=None, # type: Union[tuple, float, None] ): """Initialize a client object to talk all the OAuth2 grants to the server. Args: server_configuration (dict): It contains the configuration (i.e. metadata) of the auth server. The actual content typically contains keys like "authorization_endpoint", "token_endpoint", etc.. Based on RFC 8414 (https://tools.ietf.org/html/rfc8414), you can probably fetch it online from either https://example.com/.../.well-known/oauth-authorization-server or https://example.com/.../.well-known/openid-configuration client_id (str): The client's id, issued by the authorization server client_secret (str): Triggers HTTP AUTH for Confidential Client client_assertion (bytes, callable): The client assertion to authenticate this client, per RFC 7521. It can be a raw SAML2 assertion (this method will encode it for you), or a raw JWT assertion. It can also be a callable (recommended), so that we will do lazy creation of an assertion. client_assertion_type (str): The type of your :attr:`client_assertion` parameter. It is typically the value of :attr:`CLIENT_ASSERTION_TYPE_SAML2` or :attr:`CLIENT_ASSERTION_TYPE_JWT`, the only two defined in RFC 7521. default_headers (dict): A dict to be sent in each request header. It is not required by OAuth2 specs, but you may use it for telemetry. default_body (dict): A dict to be sent in each token request body. For example, you could choose to set this as {"client_secret": "your secret"} if your authorization server wants it to be in the request body (rather than in the request header). """ self.configuration = server_configuration self.client_id = client_id self.client_secret = client_secret self.client_assertion = client_assertion self.default_body = default_body or {} if client_assertion_type is not None: self.default_body["client_assertion_type"] = client_assertion_type self.logger = logging.getLogger(__name__) self.session = s = requests.Session() s.headers.update(default_headers or {}) s.verify = verify s.proxies = proxies or {} self.timeout = timeout def _build_auth_request_params(self, response_type, **kwargs): # response_type is a string defined in # https://tools.ietf.org/html/rfc6749#section-3.1.1 # or it can be a space-delimited string as defined in # https://tools.ietf.org/html/rfc6749#section-8.4 response_type = self._stringify(response_type) params = {'client_id': self.client_id, 'response_type': response_type} params.update(kwargs) # Note: None values will override params params = {k: v for k, v in params.items() if v is not None} # clean up if params.get('scope'): params['scope'] = self._stringify(params['scope']) return params # A dict suitable to be used in http request def _obtain_token( # The verb "obtain" is influenced by OAUTH2 RFC 6749 self, grant_type, params=None, # a dict to be sent as query string to the endpoint data=None, # All relevant data, which will go into the http body headers=None, # a dict to be sent as request headers timeout=None, post=None, # A callable to replace requests.post(), for testing. # Such as: lambda url, **kwargs: # Mock(status_code=200, json=Mock(return_value={})) **kwargs # Relay all extra parameters to underlying requests ): # Returns the json object came from the OAUTH2 response _data = {'client_id': self.client_id, 'grant_type': grant_type} if self.default_body.get("client_assertion_type") and self.client_assertion: # See https://tools.ietf.org/html/rfc7521#section-4.2 encoder = self.client_assertion_encoders.get( self.default_body["client_assertion_type"], lambda a: a) _data["client_assertion"] = encoder( self.client_assertion() # Do lazy on-the-fly computation if callable(self.client_assertion) else self.client_assertion) _data.update(self.default_body) # It may contain authen parameters _data.update(data or {}) # So the content in data param prevails # We don't have to clean up None values here, because requests lib will. if _data.get('scope'): _data['scope'] = self._stringify(_data['scope']) # Quoted from https://tools.ietf.org/html/rfc6749#section-2.3.1 # Clients in possession of a client password MAY use the HTTP Basic # authentication. # Alternatively, (but NOT RECOMMENDED,) # the authorization server MAY support including the # client credentials in the request-body using the following # parameters: client_id, client_secret. auth = None if self.client_secret and self.client_id: auth = (self.client_id, self.client_secret) # for HTTP Basic Auth if "token_endpoint" not in self.configuration: raise ValueError("token_endpoint not found in configuration") _headers = {'Accept': 'application/json'} _headers.update(headers or {}) resp = (post or self.session.post)( self.configuration["token_endpoint"], headers=_headers, params=params, data=_data, auth=auth, timeout=timeout or self.timeout, **kwargs) if resp.status_code >= 500: resp.raise_for_status() # TODO: Will probably retry here try: # The spec (https://tools.ietf.org/html/rfc6749#section-5.2) says # even an error response will be a valid json structure, # so we simply return it here, without needing to invent an exception. return resp.json() except ValueError: self.logger.exception( "Token response is not in json format: %s", resp.text) raise def obtain_token_by_refresh_token(self, refresh_token, scope=None, **kwargs): # type: (str, Union[str, list, set, tuple]) -> dict """Obtain an access token via a refresh token. :param refresh_token: The refresh token issued to the client :param scope: If omitted, is treated as equal to the scope originally granted by the resource ownser, according to https://tools.ietf.org/html/rfc6749#section-6 """ assert isinstance(refresh_token, string_types) data = kwargs.pop('data', {}) data.update(refresh_token=refresh_token, scope=scope) return self._obtain_token("refresh_token", data=data, **kwargs) def _stringify(self, sequence): if isinstance(sequence, (list, set, tuple)): return ' '.join(sorted(sequence)) # normalizing it, ascendingly return sequence # as-is class Client(BaseClient): # We choose to implement all 4 grants in 1 class """This is the main API for oauth2 client. Its methods define and document parameters mentioned in OAUTH2 RFC 6749. """ DEVICE_FLOW = { # consts for device flow, that can be customized by sub-class "GRANT_TYPE": "urn:ietf:params:oauth:grant-type:device_code", "DEVICE_CODE": "device_code", } DEVICE_FLOW_RETRIABLE_ERRORS = ("authorization_pending", "slow_down") GRANT_TYPE_SAML2 = "urn:ietf:params:oauth:grant-type:saml2-bearer" # RFC7522 GRANT_TYPE_JWT = "urn:ietf:params:oauth:grant-type:jwt-bearer" # RFC7523 grant_assertion_encoders = {GRANT_TYPE_SAML2: BaseClient.encode_saml_assertion} def initiate_device_flow(self, scope=None, timeout=None, **kwargs): # type: (list, **dict) -> dict # The naming of this method is following the wording of this specs # https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.1 """Initiate a device flow. Returns the data defined in Device Flow specs. https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.2 You should then orchestrate the User Interaction as defined in here https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.3 And possibly here https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.3.1 """ DAE = "device_authorization_endpoint" if not self.configuration.get(DAE): raise ValueError("You need to provide device authorization endpoint") flow = self.session.post(self.configuration[DAE], data={"client_id": self.client_id, "scope": self._stringify(scope or [])}, timeout=timeout or self.timeout, **kwargs).json() flow["interval"] = int(flow.get("interval", 5)) # Some IdP returns string flow["expires_in"] = int(flow.get("expires_in", 1800)) flow["expires_at"] = time.time() + flow["expires_in"] # We invent this return flow def _obtain_token_by_device_flow(self, flow, **kwargs): # type: (dict, **dict) -> dict # This method updates flow during each run. And it is non-blocking. now = time.time() skew = 1 if flow.get("latest_attempt_at", 0) + flow.get("interval", 5) - skew > now: warnings.warn('Attempted too soon. Please do time.sleep(flow["interval"])') data = kwargs.pop("data", {}) data.update({ "client_id": self.client_id, self.DEVICE_FLOW["DEVICE_CODE"]: flow["device_code"], }) result = self._obtain_token( self.DEVICE_FLOW["GRANT_TYPE"], data=data, **kwargs) if result.get("error") == "slow_down": # Respecting https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.5 flow["interval"] = flow.get("interval", 5) + 5 flow["latest_attempt_at"] = now return result def obtain_token_by_device_flow(self, flow, exit_condition=lambda flow: flow.get("expires_at", 0) < time.time(), **kwargs): # type: (dict, Callable) -> dict """Obtain token by a device flow object, with customizable polling effect. Args: flow (dict): An object previously generated by initiate_device_flow(...). Its content WILL BE CHANGED by this method during each run. We share this object with you, so that you could implement your own loop, should you choose to do so. exit_condition (Callable): This method implements a loop to provide polling effect. The loop's exit condition is calculated by this callback. The default callback makes the loop run until the flow expires. Therefore, one of the ways to exit the polling early, is to change the flow["expires_at"] to a small number such as 0. In case you are doing async programming, you may want to completely turn off the loop. You can do so by using a callback as: exit_condition = lambda flow: True to make the loop run only once, i.e. no polling, hence non-block. """ while True: result = self._obtain_token_by_device_flow(flow, **kwargs) if result.get("error") not in self.DEVICE_FLOW_RETRIABLE_ERRORS: return result for i in range(flow.get("interval", 5)): # Wait interval seconds if exit_condition(flow): return result time.sleep(1) # Shorten each round, to make exit more responsive def build_auth_request_uri( self, response_type, redirect_uri=None, scope=None, state=None, **kwargs): """Generate an authorization uri to be visited by resource owner. Later when the response reaches your redirect_uri, you can use parse_auth_response() to check the returned state. This method could be named build_authorization_request_uri() instead, but then there would be a build_authentication_request_uri() in the OIDC subclass doing almost the same thing. So we use a loose term "auth" here. :param response_type: Must be "code" when you are using Authorization Code Grant, "token" when you are using Implicit Grant, or other (possibly space-delimited) strings as registered extension value. See https://tools.ietf.org/html/rfc6749#section-3.1.1 :param redirect_uri: Optional. Server will use the pre-registered one. :param scope: It is a space-delimited, case-sensitive string. Some ID provider can accept empty string to represent default scope. :param state: Recommended. An opaque value used by the client to maintain state between the request and callback. :param kwargs: Other parameters, typically defined in OpenID Connect. """ if "authorization_endpoint" not in self.configuration: raise ValueError("authorization_endpoint not found in configuration") authorization_endpoint = self.configuration["authorization_endpoint"] params = self._build_auth_request_params( response_type, redirect_uri=redirect_uri, scope=scope, state=state, **kwargs) sep = '&' if '?' in authorization_endpoint else '?' return "%s%s%s" % (authorization_endpoint, sep, urlencode(params)) @staticmethod def parse_auth_response(params, state=None): """Parse the authorization response being redirected back. :param params: A string or dict of the query string :param state: REQUIRED if the state parameter was present in the client authorization request. This function will compare it with response. """ if not isinstance(params, dict): params = parse_qs(params) if params.get('state') != state: raise ValueError('state mismatch') return params def obtain_token_by_authorization_code( self, code, redirect_uri=None, scope=None, **kwargs): """Get a token via auhtorization code. a.k.a. Authorization Code Grant. This is typically used by a server-side app (Confidential Client), but it can also be used by a device-side native app (Public Client). See more detail at https://tools.ietf.org/html/rfc6749#section-4.1.3 :param code: The authorization code received from authorization server. :param redirect_uri: Required, if the "redirect_uri" parameter was included in the authorization request, and their values MUST be identical. :param scope: It is both unnecessary and harmless to use scope here, per RFC 6749. We suggest to use the same scope already used in auth request uri, so that this library can link the obtained tokens with their scope. """ data = kwargs.pop("data", {}) data.update(code=code, redirect_uri=redirect_uri) if scope: data["scope"] = scope if not self.client_secret: # client_id is required, if the client is not authenticating itself. # See https://tools.ietf.org/html/rfc6749#section-4.1.3 data["client_id"] = self.client_id return self._obtain_token("authorization_code", data=data, **kwargs) def obtain_token_by_username_password( self, username, password, scope=None, **kwargs): """The Resource Owner Password Credentials Grant, used by legacy app.""" data = kwargs.pop("data", {}) data.update(username=username, password=password, scope=scope) return self._obtain_token("password", data=data, **kwargs) def obtain_token_for_client(self, scope=None, **kwargs): """Obtain token for this client (rather than for an end user), a.k.a. the Client Credentials Grant, used by Backend Applications. We don't name it obtain_token_by_client_credentials(...) because those credentials are typically already provided in class constructor, not here. You can still explicitly provide an optional client_secret parameter, or you can provide such extra parameters as `default_body` during the class initialization. """ data = kwargs.pop("data", {}) data.update(scope=scope) return self._obtain_token("client_credentials", data=data, **kwargs) def __init__(self, server_configuration, client_id, on_obtaining_tokens=lambda event: None, # event is defined in _obtain_token(...) on_removing_rt=lambda token_item: None, on_updating_rt=lambda token_item, new_rt: None, **kwargs): super(Client, self).__init__(server_configuration, client_id, **kwargs) self.on_obtaining_tokens = on_obtaining_tokens self.on_removing_rt = on_removing_rt self.on_updating_rt = on_updating_rt def _obtain_token(self, grant_type, params=None, data=None, *args, **kwargs): RT = "refresh_token" _data = data.copy() # to prevent side effect refresh_token = _data.get(RT) resp = super(Client, self)._obtain_token( grant_type, params, _data, *args, **kwargs) if "error" not in resp: _resp = resp.copy() if grant_type == RT and RT in _resp and isinstance(refresh_token, dict): _resp.pop(RT) # So we skip it in on_obtaining_tokens(); it will # be handled in self.obtain_token_by_refresh_token() if "scope" in _resp: scope = _resp["scope"].split() # It is conceptually a set, # but we represent it as a list which can be persisted to JSON else: # Note: The scope will generally be absent in authorization grant, # but our obtain_token_by_authorization_code(...) encourages # app developer to still explicitly provide a scope here. scope = _data.get("scope") self.on_obtaining_tokens({ "client_id": self.client_id, "scope": scope, "token_endpoint": self.configuration["token_endpoint"], "grant_type": grant_type, # can be used to know an IdToken-less # response is for an app or for a user "response": _resp, "params": params, "data": _data, }) return resp def obtain_token_by_refresh_token(self, token_item, scope=None, rt_getter=lambda token_item: token_item["refresh_token"], on_removing_rt=None, **kwargs): # type: (Union[str, dict], Union[str, list, set, tuple], Callable) -> dict """This is an overload which will trigger token storage callbacks. :param token_item: A refresh token (RT) item, in flexible format. It can be a string, or a whatever data structure containing RT string and its metadata, in such case the `rt_getter` callable must be able to extract the RT string out from the token item data structure. Either way, this token_item will be passed into other callbacks as-is. :param scope: If omitted, is treated as equal to the scope originally granted by the resource ownser, according to https://tools.ietf.org/html/rfc6749#section-6 :param rt_getter: A callable to translate the token_item to a raw RT string :param on_removing_rt: If absent, fall back to the one defined in initialization """ resp = super(Client, self).obtain_token_by_refresh_token( rt_getter(token_item) if not isinstance(token_item, string_types) else token_item, scope=scope, **kwargs) if resp.get('error') == 'invalid_grant': (on_removing_rt or self.on_removing_rt)(token_item) # Discard old RT if 'refresh_token' in resp: self.on_updating_rt(token_item, resp['refresh_token']) return resp def obtain_token_by_assertion( self, assertion, grant_type, scope=None, **kwargs): # type: (bytes, Union[str, None], Union[str, list, set, tuple]) -> dict """This method implements Assertion Framework for OAuth2 (RFC 7521). See details at https://tools.ietf.org/html/rfc7521#section-4.1 :param assertion: The assertion bytes can be a raw SAML2 assertion, or a JWT assertion. :param grant_type: It is typically either the value of :attr:`GRANT_TYPE_SAML2`, or :attr:`GRANT_TYPE_JWT`, the only two profiles defined in RFC 7521. :param scope: Optional. It must be a subset of previously granted scopes. """ encoder = self.grant_assertion_encoders.get(grant_type, lambda a: a) data = kwargs.pop("data", {}) data.update(scope=scope, assertion=encoder(assertion)) return self._obtain_token(grant_type, data=data, **kwargs) microsoft-authentication-library-for-python-1.1.0/msal/oauth2cli/oidc.py000066400000000000000000000073141361242362500264270ustar00rootroot00000000000000import json import base64 import time from . import oauth2 def decode_part(raw, encoding="utf-8"): """Decode a part of the JWT. JWT is encoded by padding-less base64url, based on `JWS specs `_. :param encoding: If you are going to decode the first 2 parts of a JWT, i.e. the header or the payload, the default value "utf-8" would work fine. If you are going to decode the last part i.e. the signature part, it is a binary string so you should use `None` as encoding here. """ raw += '=' * (-len(raw) % 4) # https://stackoverflow.com/a/32517907/728675 raw = str( # On Python 2.7, argument of urlsafe_b64decode must be str, not unicode. # This is not required on Python 3. raw) output = base64.urlsafe_b64decode(raw) if encoding: output = output.decode(encoding) return output base64decode = decode_part # Obsolete. For backward compatibility only. def decode_id_token(id_token, client_id=None, issuer=None, nonce=None, now=None): """Decodes and validates an id_token and returns its claims as a dictionary. ID token claims would at least contain: "iss", "sub", "aud", "exp", "iat", per `specs `_ and it may contain other optional content such as "preferred_username", `maybe more `_ """ decoded = json.loads(decode_part(id_token.split('.')[1])) err = None # https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation if issuer and issuer != decoded["iss"]: # https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse err = ('2. The Issuer Identifier for the OpenID Provider, "%s", ' "(which is typically obtained during Discovery), " "MUST exactly match the value of the iss (issuer) Claim.") % issuer if client_id: valid_aud = client_id in decoded["aud"] if isinstance( decoded["aud"], list) else client_id == decoded["aud"] if not valid_aud: err = "3. The aud (audience) Claim must contain this client's client_id." # Per specs: # 6. If the ID Token is received via direct communication between # the Client and the Token Endpoint (which it is in this flow), # the TLS server validation MAY be used to validate the issuer # in place of checking the token signature. if (now or time.time()) > decoded["exp"]: err = "9. The current time MUST be before the time represented by the exp Claim." if nonce and nonce != decoded.get("nonce"): err = ("11. Nonce must be the same value " "as the one that was sent in the Authentication Request") if err: raise RuntimeError("%s id_token was: %s" % ( err, json.dumps(decoded, indent=2))) return decoded class Client(oauth2.Client): """OpenID Connect is a layer on top of the OAuth2. See its specs at https://openid.net/connect/ """ def decode_id_token(self, id_token, nonce=None): """See :func:`~decode_id_token`.""" return decode_id_token( id_token, nonce=nonce, client_id=self.client_id, issuer=self.configuration.get("issuer")) def _obtain_token(self, grant_type, *args, **kwargs): """The result will also contain one more key "id_token_claims", whose value will be a dictionary returned by :func:`~decode_id_token`. """ ret = super(Client, self)._obtain_token(grant_type, *args, **kwargs) if "id_token" in ret: ret["id_token_claims"] = self.decode_id_token(ret["id_token"]) return ret microsoft-authentication-library-for-python-1.1.0/msal/token_cache.py000066400000000000000000000327761361242362500260740ustar00rootroot00000000000000import json import threading import time import logging from .authority import canonicalize from .oauth2cli.oidc import decode_part, decode_id_token logger = logging.getLogger(__name__) def is_subdict_of(small, big): return dict(big, **small) == big class TokenCache(object): """This is considered as a base class containing minimal cache behavior. Although it maintains tokens using unified schema across all MSAL libraries, this class does not serialize/persist them. See subclass :class:`SerializableTokenCache` for details on serialization. """ class CredentialType: ACCESS_TOKEN = "AccessToken" REFRESH_TOKEN = "RefreshToken" ACCOUNT = "Account" # Not exactly a credential type, but we put it here ID_TOKEN = "IdToken" APP_METADATA = "AppMetadata" class AuthorityType: ADFS = "ADFS" MSSTS = "MSSTS" # MSSTS means AAD v2 for both AAD & MSA def __init__(self): self._lock = threading.RLock() self._cache = {} self.key_makers = { self.CredentialType.REFRESH_TOKEN: lambda home_account_id=None, environment=None, client_id=None, target=None, **ignored_payload_from_a_real_token: "-".join([ home_account_id or "", environment or "", self.CredentialType.REFRESH_TOKEN, client_id or "", "", # RT is cross-tenant in AAD target or "", # raw value could be None if deserialized from other SDK ]).lower(), self.CredentialType.ACCESS_TOKEN: lambda home_account_id=None, environment=None, client_id=None, realm=None, target=None, **ignored_payload_from_a_real_token: "-".join([ home_account_id or "", environment or "", self.CredentialType.ACCESS_TOKEN, client_id or "", realm or "", target or "", ]).lower(), self.CredentialType.ID_TOKEN: lambda home_account_id=None, environment=None, client_id=None, realm=None, **ignored_payload_from_a_real_token: "-".join([ home_account_id or "", environment or "", self.CredentialType.ID_TOKEN, client_id or "", realm or "", "" # Albeit irrelevant, schema requires an empty scope here ]).lower(), self.CredentialType.ACCOUNT: lambda home_account_id=None, environment=None, realm=None, **ignored_payload_from_a_real_entry: "-".join([ home_account_id or "", environment or "", realm or "", ]).lower(), self.CredentialType.APP_METADATA: lambda environment=None, client_id=None, **kwargs: "appmetadata-{}-{}".format(environment or "", client_id or ""), } def find(self, credential_type, target=None, query=None): target = target or [] assert isinstance(target, list), "Invalid parameter type" target_set = set(target) with self._lock: # Since the target inside token cache key is (per schema) unsorted, # there is no point to attempt an O(1) key-value search here. # So we always do an O(n) in-memory search. return [entry for entry in self._cache.get(credential_type, {}).values() if is_subdict_of(query or {}, entry) and (target_set <= set(entry.get("target", "").split()) if target else True) ] def add(self, event, now=None): # type: (dict) -> None """Handle a token obtaining event, and add tokens into cache. Known side effects: This function modifies the input event in place. """ def wipe(dictionary, sensitive_fields): # Masks sensitive info for sensitive in sensitive_fields: if sensitive in dictionary: dictionary[sensitive] = "********" wipe(event.get("data", {}), ("password", "client_secret", "refresh_token", "assertion", "username")) try: return self.__add(event, now=now) finally: wipe(event.get("response", {}), ("access_token", "refresh_token")) logger.debug("event=%s", json.dumps( # We examined and concluded that this log won't have Log Injection risk, # because the event payload is already in JSON so CR/LF will be escaped. event, indent=4, sort_keys=True, default=str, # A workaround when assertion is in bytes in Python 3 )) def __add(self, event, now=None): # event typically contains: client_id, scope, token_endpoint, # response, params, data, grant_type environment = realm = None if "token_endpoint" in event: _, environment, realm = canonicalize(event["token_endpoint"]) response = event.get("response", {}) data = event.get("data", {}) access_token = response.get("access_token") refresh_token = response.get("refresh_token") id_token = response.get("id_token") id_token_claims = ( decode_id_token(id_token, client_id=event["client_id"]) if id_token else {}) client_info = {} home_account_id = None # It would remain None in client_credentials flow if "client_info" in response: # We asked for it, and AAD will provide it client_info = json.loads(decode_part(response["client_info"])) home_account_id = "{uid}.{utid}".format(**client_info) elif id_token_claims: # This would be an end user on ADFS-direct scenario client_info["uid"] = id_token_claims.get("sub") home_account_id = id_token_claims.get("sub") target = ' '.join(event.get("scope", [])) # Per schema, we don't sort it with self._lock: if access_token: now = int(time.time() if now is None else now) expires_in = int( # AADv1-like endpoint returns a string response.get("expires_in", 3599)) ext_expires_in = int( # AADv1-like endpoint returns a string response.get("ext_expires_in", expires_in)) at = { "credential_type": self.CredentialType.ACCESS_TOKEN, "secret": access_token, "home_account_id": home_account_id, "environment": environment, "client_id": event.get("client_id"), "target": target, "realm": realm, "token_type": response.get("token_type", "Bearer"), "cached_at": str(now), # Schema defines it as a string "expires_on": str(now + expires_in), # Same here "extended_expires_on": str(now + ext_expires_in) # Same here } if data.get("key_id"): # It happens in SSH-cert or POP scenario at["key_id"] = data.get("key_id") self.modify(self.CredentialType.ACCESS_TOKEN, at, at) if client_info: account = { "home_account_id": home_account_id, "environment": environment, "realm": realm, "local_account_id": id_token_claims.get( "oid", id_token_claims.get("sub")), "username": id_token_claims.get("preferred_username") # AAD or id_token_claims.get("upn") # ADFS 2019 or "", # The schema does not like null "authority_type": self.AuthorityType.ADFS if realm == "adfs" else self.AuthorityType.MSSTS, # "client_info": response.get("client_info"), # Optional } self.modify(self.CredentialType.ACCOUNT, account, account) if id_token: idt = { "credential_type": self.CredentialType.ID_TOKEN, "secret": id_token, "home_account_id": home_account_id, "environment": environment, "realm": realm, "client_id": event.get("client_id"), # "authority": "it is optional", } self.modify(self.CredentialType.ID_TOKEN, idt, idt) if refresh_token: rt = { "credential_type": self.CredentialType.REFRESH_TOKEN, "secret": refresh_token, "home_account_id": home_account_id, "environment": environment, "client_id": event.get("client_id"), "target": target, # Optional per schema though } if "foci" in response: rt["family_id"] = response["foci"] self.modify(self.CredentialType.REFRESH_TOKEN, rt, rt) app_metadata = { "client_id": event.get("client_id"), "environment": environment, } if "foci" in response: app_metadata["family_id"] = response.get("foci") self.modify(self.CredentialType.APP_METADATA, app_metadata, app_metadata) def modify(self, credential_type, old_entry, new_key_value_pairs=None): # Modify the specified old_entry with new_key_value_pairs, # or remove the old_entry if the new_key_value_pairs is None. # This helper exists to consolidate all token add/modify/remove behaviors, # so that the sub-classes will have only one method to work on, # instead of patching a pair of update_xx() and remove_xx() per type. # You can monkeypatch self.key_makers to support more types on-the-fly. key = self.key_makers[credential_type](**old_entry) with self._lock: if new_key_value_pairs: # Update with them entries = self._cache.setdefault(credential_type, {}) entry = entries.setdefault(key, {}) # Create it if not yet exist entry.update(new_key_value_pairs) else: # Remove old_entry self._cache.setdefault(credential_type, {}).pop(key, None) def remove_rt(self, rt_item): assert rt_item.get("credential_type") == self.CredentialType.REFRESH_TOKEN return self.modify(self.CredentialType.REFRESH_TOKEN, rt_item) def update_rt(self, rt_item, new_rt): assert rt_item.get("credential_type") == self.CredentialType.REFRESH_TOKEN return self.modify( self.CredentialType.REFRESH_TOKEN, rt_item, {"secret": new_rt}) def remove_at(self, at_item): assert at_item.get("credential_type") == self.CredentialType.ACCESS_TOKEN return self.modify(self.CredentialType.ACCESS_TOKEN, at_item) def remove_idt(self, idt_item): assert idt_item.get("credential_type") == self.CredentialType.ID_TOKEN return self.modify(self.CredentialType.ID_TOKEN, idt_item) def remove_account(self, account_item): assert "authority_type" in account_item return self.modify(self.CredentialType.ACCOUNT, account_item) class SerializableTokenCache(TokenCache): """This serialization can be a starting point to implement your own persistence. This class does NOT actually persist the cache on disk/db/etc.. Depending on your need, the following simple recipe for file-based persistence may be sufficient:: import os, atexit, msal cache = msal.SerializableTokenCache() if os.path.exists("my_cache.bin"): cache.deserialize(open("my_cache.bin", "r").read()) atexit.register(lambda: open("my_cache.bin", "w").write(cache.serialize()) # Hint: The following optional line persists only when state changed if cache.has_state_changed else None ) app = msal.ClientApplication(..., token_cache=cache) ... :var bool has_state_changed: Indicates whether the cache state in the memory has changed since last :func:`~serialize` or :func:`~deserialize` call. """ has_state_changed = False def add(self, event, **kwargs): super(SerializableTokenCache, self).add(event, **kwargs) self.has_state_changed = True def modify(self, credential_type, old_entry, new_key_value_pairs=None): super(SerializableTokenCache, self).modify( credential_type, old_entry, new_key_value_pairs) self.has_state_changed = True def deserialize(self, state): # type: (Optional[str]) -> None """Deserialize the cache from a state previously obtained by serialize()""" with self._lock: self._cache = json.loads(state) if state else {} self.has_state_changed = False # reset def serialize(self): # type: () -> str """Serialize the current cache state into a string.""" with self._lock: self.has_state_changed = False return json.dumps(self._cache, indent=4) microsoft-authentication-library-for-python-1.1.0/msal/wstrust_request.py000066400000000000000000000136121361242362500271200ustar00rootroot00000000000000#------------------------------------------------------------------------------ # # Copyright (c) Microsoft Corporation. # All rights reserved. # # This code is licensed under the MIT License. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files(the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions : # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #------------------------------------------------------------------------------ import uuid from datetime import datetime, timedelta import logging import requests from .mex import Mex from .wstrust_response import parse_response logger = logging.getLogger(__name__) def send_request( username, password, cloud_audience_urn, endpoint_address, soap_action, **kwargs): if not endpoint_address: raise ValueError("WsTrust endpoint address can not be empty") if soap_action is None: if '/trust/2005/usernamemixed' in endpoint_address: soap_action = Mex.ACTION_2005 elif '/trust/13/usernamemixed' in endpoint_address: soap_action = Mex.ACTION_13 assert soap_action in (Mex.ACTION_13, Mex.ACTION_2005), ( # A loose check here "Unsupported soap action: %s" % soap_action) data = _build_rst( username, password, cloud_audience_urn, endpoint_address, soap_action) resp = requests.post(endpoint_address, data=data, headers={ 'Content-type':'application/soap+xml; charset=utf-8', 'SOAPAction': soap_action, }, **kwargs) if resp.status_code >= 400: logger.debug("Unsuccessful WsTrust request receives: %s", resp.text) # It turns out ADFS uses 5xx status code even with client-side incorrect password error # resp.raise_for_status() return parse_response(resp.text) def escape_password(password): return (password.replace('&', '&').replace('"', '"') .replace("'", ''') # the only one not provided by cgi.escape(s, True) .replace('<', '<').replace('>', '>')) def wsu_time_format(datetime_obj): # WsTrust (http://docs.oasis-open.org/ws-sx/ws-trust/v1.4/ws-trust.html) # does not seem to define timestamp format, but we see YYYY-mm-ddTHH:MM:SSZ # here (https://www.ibm.com/developerworks/websphere/library/techarticles/1003_chades/1003_chades.html) # It avoids the uncertainty of the optional ".ssssss" in datetime.isoformat() # https://docs.python.org/2/library/datetime.html#datetime.datetime.isoformat return datetime_obj.strftime('%Y-%m-%dT%H:%M:%SZ') def _build_rst(username, password, cloud_audience_urn, endpoint_address, soap_action): now = datetime.utcnow() return """ {soap_action} urn:uuid:{message_id} http://www.w3.org/2005/08/addressing/anonymous {endpoint_address} {time_now} {time_expire} {username} {password} {applies_to} {key_type} {request_type} """.format( s=Mex.NS["s"], wsu=Mex.NS["wsu"], wsa=Mex.NS["wsa10"], soap_action=soap_action, message_id=str(uuid.uuid4()), endpoint_address=endpoint_address, time_now=wsu_time_format(now), time_expire=wsu_time_format(now + timedelta(minutes=10)), username=username, password=escape_password(password), wst=Mex.NS["wst"] if soap_action == Mex.ACTION_13 else Mex.NS["wst2005"], applies_to=cloud_audience_urn, key_type='http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer' if soap_action == Mex.ACTION_13 else 'http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey', request_type='http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue' if soap_action == Mex.ACTION_13 else 'http://schemas.xmlsoap.org/ws/2005/02/trust/Issue', ) microsoft-authentication-library-for-python-1.1.0/msal/wstrust_response.py000066400000000000000000000105011361242362500272600ustar00rootroot00000000000000#------------------------------------------------------------------------------ # # Copyright (c) Microsoft Corporation. # All rights reserved. # # This code is licensed under the MIT License. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files(the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions : # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #------------------------------------------------------------------------------ try: from xml.etree import cElementTree as ET except ImportError: from xml.etree import ElementTree as ET import re from .mex import Mex SAML_TOKEN_TYPE_V1 = 'urn:oasis:names:tc:SAML:1.0:assertion' SAML_TOKEN_TYPE_V2 = 'urn:oasis:names:tc:SAML:2.0:assertion' # http://docs.oasis-open.org/wss-m/wss/v1.1.1/os/wss-SAMLTokenProfile-v1.1.1-os.html#_Toc307397288 WSS_SAML_TOKEN_PROFILE_V1_1 = "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" WSS_SAML_TOKEN_PROFILE_V2 = "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" def parse_response(body): # Returns {"token": "", "type": "..."} token = parse_token_by_re(body) if token: return token error = parse_error(body) raise RuntimeError("WsTrust server returned error in RSTR: %s" % (error or body)) def parse_error(body): # Returns error as a dict. See unit test case for an example. dom = ET.fromstring(body) reason_text_node = dom.find('s:Body/s:Fault/s:Reason/s:Text', Mex.NS) subcode_value_node = dom.find('s:Body/s:Fault/s:Code/s:Subcode/s:Value', Mex.NS) if reason_text_node is not None or subcode_value_node is not None: return {"reason": reason_text_node.text, "code": subcode_value_node.text} def findall_content(xml_string, tag): """ Given a tag name without any prefix, this function returns a list of the raw content inside this tag as-is. >>> findall_content(" what ever content ", "foo") [" what ever content "] Motivation: Usually we would use XML parser to extract the data by xpath. However the ElementTree in Python will implicitly normalize the output by "hoisting" the inner inline namespaces into the outmost element. The result will be a semantically equivalent XML snippet, but not fully identical to the original one. While this effect shouldn't become a problem in all other cases, it does not seem to fully comply with Exclusive XML Canonicalization spec (https://www.w3.org/TR/xml-exc-c14n/), and void the SAML token signature. SAML signature algo needs the "XML -> C14N(XML) -> Signed(C14N(Xml))" order. The binary extention lxml is probably the canonical way to solve this (https://stackoverflow.com/questions/22959577/python-exclusive-xml-canonicalization-xml-exc-c14n) but here we use this workaround, based on Regex, to return raw content as-is. """ # \w+ is good enough for https://www.w3.org/TR/REC-xml/#NT-NameChar pattern = r"<(?:\w+:)?%(tag)s(?:[^>]*)>(.*)=2.0.0,<3', 'PyJWT[crypto]>=1.0.0,<2', ] ) microsoft-authentication-library-for-python-1.1.0/tests/000077500000000000000000000000001361242362500234465ustar00rootroot00000000000000microsoft-authentication-library-for-python-1.1.0/tests/__init__.py000066400000000000000000000015621361242362500255630ustar00rootroot00000000000000import sys import logging if sys.version_info[:2] < (2, 7): # The unittest module got a significant overhaul in Python 2.7, # so if we're in 2.6 we can use the backported version unittest2. import unittest2 as unittest else: import unittest class Oauth2TestCase(unittest.TestCase): logger = logging.getLogger(__file__) def assertLoosely(self, response, assertion=None, skippable_errors=("invalid_grant", "interaction_required")): if response.get("error") in skippable_errors: self.logger.debug("Response = %s", response) # Some of these errors are configuration issues, not library issues raise unittest.SkipTest(response.get("error_description")) else: if assertion is None: assertion = lambda: self.assertIn("access_token", response) assertion() microsoft-authentication-library-for-python-1.1.0/tests/archan.us.mex.xml000066400000000000000000001457541361242362500266620ustar00rootroot00000000000000 http://schemas.xmlsoap.org/ws/2005/02/trust/PublicKey http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2000/09/xmldsig#rsa-sha1 http://www.w3.org/2001/10/xml-exc-c14n# http://www.w3.org/2001/04/xmlenc#aes256-cbc http://schemas.xmlsoap.org/ws/2005/02/trust/SymmetricKey 256 http://www.w3.org/2001/04/xmlenc#aes256-cbc http://www.w3.org/2000/09/xmldsig#hmac-sha1 http://www.w3.org/2001/10/xml-exc-c14n# http://www.w3.org/2001/04/xmlenc#aes256-cbc http://docs.oasis-open.org/ws-sx/ws-trust/200512/PublicKey http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2000/09/xmldsig#rsa-sha1 http://www.w3.org/2001/10/xml-exc-c14n# http://www.w3.org/2001/04/xmlenc#aes256-cbc http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey 256 http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2001/04/xmlenc#aes256-cbc http://www.w3.org/2000/09/xmldsig#hmac-sha1 http://www.w3.org/2001/10/xml-exc-c14n# http://www.w3.org/2001/04/xmlenc#aes256-cbc https://arvmserver2012.archan.us/adfs/services/trust/2005/windowstransport host/ARVMServer2012.archan.us https://arvmserver2012.archan.us/adfs/services/trust/2005/certificatemixed https://arvmserver2012.archan.us/adfs/services/trust/2005/certificatetransport https://arvmserver2012.archan.us/adfs/services/trust/2005/usernamemixed https://arvmserver2012.archan.us/adfs/services/trust/2005/kerberosmixed https://arvmserver2012.archan.us/adfs/services/trust/2005/issuedtokenmixedasymmetricbasic256 https://arvmserver2012.archan.us/adfs/services/trust/2005/issuedtokenmixedsymmetricbasic256 https://arvmserver2012.archan.us/adfs/services/trust/13/kerberosmixed https://arvmserver2012.archan.us/adfs/services/trust/13/certificatemixed https://arvmserver2012.archan.us/adfs/services/trust/13/usernamemixed https://arvmserver2012.archan.us/adfs/services/trust/13/issuedtokenmixedasymmetricbasic256 https://arvmserver2012.archan.us/adfs/services/trust/13/issuedtokenmixedsymmetricbasic256 https://arvmserver2012.archan.us/adfs/services/trust/13/windowstransport host/ARVMServer2012.archan.us microsoft-authentication-library-for-python-1.1.0/tests/arupela.mex.xml000066400000000000000000001404651361242362500264230ustar00rootroot00000000000000 http://schemas.xmlsoap.org/ws/2005/02/trust/PublicKey http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2000/09/xmldsig#rsa-sha1 http://www.w3.org/2001/10/xml-exc-c14n# http://www.w3.org/2001/04/xmlenc#aes256-cbc http://schemas.xmlsoap.org/ws/2005/02/trust/SymmetricKey 256 http://www.w3.org/2001/04/xmlenc#aes256-cbc http://www.w3.org/2000/09/xmldsig#hmac-sha1 http://www.w3.org/2001/10/xml-exc-c14n# http://www.w3.org/2001/04/xmlenc#aes256-cbc http://docs.oasis-open.org/ws-sx/ws-trust/200512/PublicKey http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2000/09/xmldsig#rsa-sha1 http://www.w3.org/2001/10/xml-exc-c14n# http://www.w3.org/2001/04/xmlenc#aes256-cbc http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey 256 http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2001/04/xmlenc#aes256-cbc http://www.w3.org/2000/09/xmldsig#hmac-sha1 http://www.w3.org/2001/10/xml-exc-c14n# http://www.w3.org/2001/04/xmlenc#aes256-cbc https://fs.arupela.com/adfs/services/trust/2005/windowstransport host/fs.arupela.com https://fs.arupela.com/adfs/services/trust/2005/certificatemixed https://fs.arupela.com:49443/adfs/services/trust/2005/certificatetransport https://fs.arupela.com/adfs/services/trust/2005/usernamemixed https://fs.arupela.com/adfs/services/trust/2005/kerberosmixed https://fs.arupela.com/adfs/services/trust/2005/issuedtokenmixedasymmetricbasic256 https://fs.arupela.com/adfs/services/trust/2005/issuedtokenmixedsymmetricbasic256 https://fs.arupela.com/adfs/services/trust/13/kerberosmixed https://fs.arupela.com/adfs/services/trust/13/certificatemixed https://fs.arupela.com/adfs/services/trust/13/usernamemixed https://fs.arupela.com/adfs/services/trust/13/issuedtokenmixedasymmetricbasic256 https://fs.arupela.com/adfs/services/trust/13/issuedtokenmixedsymmetricbasic256 microsoft-authentication-library-for-python-1.1.0/tests/authcode.py000066400000000000000000000071211361242362500256150ustar00rootroot00000000000000import argparse import webbrowser import logging try: # Python 3 from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs, urlencode except ImportError: # Fall back to Python 2 from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from urlparse import urlparse, parse_qs from urllib import urlencode def build_auth_url(authority, client_id): # Lucky that redirect_uri can be omitted, so it works for any app return "{a}/oauth2/authorize?response_type=code&client_id={c}".format( a=authority, c=client_id) class AuthCodeReceiver(BaseHTTPRequestHandler): """A one-stop solution to acquire an authorization code. This helper starts a web server as redirect_uri, waiting for auth code. It also opens a browser window to guide a human tester to manually login. After obtaining an auth code, the web server will be shut down. """ # Note: This docstring is also used by this script's command line help. @classmethod def acquire(cls, auth_endpoint, redirect_port): """Usage: ac = AuthCodeReceiver.acquire('http://.../authorize', 8088)""" webbrowser.open( "http://localhost:{p}?{q}".format(p=redirect_port, q=urlencode({ "text": """Open this link to acquire auth code. If you prefer, you may want to use incognito window.""", "link": auth_endpoint,}))) logging.warn( """Listening on http://localhost:{}, and a browser window is opened for you on THIS machine, and waiting for human interaction. This function call will hang until an auth code is received. """.format(redirect_port)) server = HTTPServer(("", int(redirect_port)), cls) server.authcode = None while not server.authcode: # https://docs.python.org/2/library/basehttpserver.html#more-examples server.handle_request() return server.authcode def do_GET(self): # For flexibility, we choose to not check self.path matching redirect_uri #assert self.path.startswith('/THE_PATH_REGISTERED_BY_THE_APP') qs = parse_qs(urlparse(self.path).query) if qs.get('code'): # Then store it into the server instance ac = self.server.authcode = qs['code'][0] self.send_full_response('Authcode:\n{}'.format(ac)) # NOTE: Don't do self.server.shutdown() here. It'll halt the server. elif qs.get('text') and qs.get('link'): # Then display a landing page self.send_full_response('{text}'.format( link=qs['link'][0], text=qs['text'][0])) else: self.send_full_response("This web service serves your redirect_uri") def send_full_response(self, body, is_ok=True): self.send_response(200 if is_ok else 400) content_type = 'text/html' if body.startswith('<') else 'text/plain' self.send_header('Content-type', content_type) self.end_headers() self.wfile.write(body) if __name__ == '__main__': p = parser = argparse.ArgumentParser( description=AuthCodeReceiver.__doc__ + "The auth code received will be dumped into stdout.") p.add_argument('client_id', help="The client_id of your web service app") p.add_argument('redirect_port', type=int, help="The port in redirect_uri") p.add_argument( "--authority", default="https://login.microsoftonline.com/common") args = parser.parse_args() print(AuthCodeReceiver.acquire( build_auth_url(args.authority, args.client_id), args.redirect_port)) microsoft-authentication-library-for-python-1.1.0/tests/microsoft.mex.xml000066400000000000000000001461141361242362500267740ustar00rootroot00000000000000 http://schemas.xmlsoap.org/ws/2005/02/trust/PublicKey http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2000/09/xmldsig#rsa-sha1 http://www.w3.org/2001/10/xml-exc-c14n# http://www.w3.org/2001/04/xmlenc#aes256-cbc http://schemas.xmlsoap.org/ws/2005/02/trust/SymmetricKey 256 http://www.w3.org/2001/04/xmlenc#aes256-cbc http://www.w3.org/2000/09/xmldsig#hmac-sha1 http://www.w3.org/2001/10/xml-exc-c14n# http://www.w3.org/2001/04/xmlenc#aes256-cbc http://docs.oasis-open.org/ws-sx/ws-trust/200512/PublicKey http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2000/09/xmldsig#rsa-sha1 http://www.w3.org/2001/10/xml-exc-c14n# http://www.w3.org/2001/04/xmlenc#aes256-cbc http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey 256 http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p http://www.w3.org/2001/04/xmlenc#aes256-cbc http://www.w3.org/2000/09/xmldsig#hmac-sha1 http://www.w3.org/2001/10/xml-exc-c14n# http://www.w3.org/2001/04/xmlenc#aes256-cbc https://corp.sts.microsoft.com/adfs/services/trust/2005/windowstransport iamfed@redmond.corp.microsoft.com https://corp.sts.microsoft.com/adfs/services/trust/2005/certificatemixed https://corp.sts.microsoft.com/adfs/services/trust/2005/usernamemixed https://corp.sts.microsoft.com/adfs/services/trust/2005/kerberosmixed https://corp.sts.microsoft.com/adfs/services/trust/2005/issuedtokenmixedasymmetricbasic256 https://corp.sts.microsoft.com/adfs/services/trust/2005/issuedtokenmixedsymmetricbasic256 https://corp.sts.microsoft.com/adfs/services/trust/13/kerberosmixed https://corp.sts.microsoft.com/adfs/services/trust/13/certificatemixed https://corp.sts.microsoft.com/adfs/services/trust/13/usernamemixed https://corp.sts.microsoft.com/adfs/services/trust/13/issuedtokenmixedasymmetricbasic256 https://corp.sts.microsoft.com/adfs/services/trust/13/issuedtokenmixedsymmetricbasic256 https://corp.sts.microsoft.com/adfs/services/trust/13/windowstransport iamfed@redmond.corp.microsoft.com microsoft-authentication-library-for-python-1.1.0/tests/rst_response.xml000066400000000000000000000212151361242362500267170ustar00rootroot00000000000000 http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal 2013-11-15T03:08:25.221Z 2013-11-15T03:13:25.221Z 2013-11-15T03:08:25.205Z 2013-11-15T04:08:25.205Z https://login.microsoftonline.com/extSTS.srf https://login.microsoftonline.com/extSTS.srf 1TIu064jGEmmf+hnI+F0Jg== urn:oasis:names:tc:SAML:1.0:cm:bearer frizzo@richard-randall.com 1TIu064jGEmmf+hnI+F0Jg== 1TIu064jGEmmf+hnI+F0Jg== urn:oasis:names:tc:SAML:1.0:cm:bearer 3i95D+nRbsyRitSPeT7ZtEr5vbM= aVNmmKLNdAlBxxcNciWVfxynZUPR9ql8ZZSZt/qpqL/GB3HX/cL/QnfG2OOKrmhgEaR0Ul4grZhGJxlxMPDL0fhnBz+VJ5HwztMFgMYs3Md8A2sZd9n4dfu7+CByAna06lCwwfdFWlNV1MBFvlWvYtCLNkpYVr/aglmb9zpMkNxEOmHe/cwxUtYlzH4RpIsIT5pruoJtUxKcqTRDEeeYdzjBAiJuguQTChLmHNoMPdX1RmtJlPsrZ1s9R/IJky7fHLjB7jiTDceRCS5QUbgUqYbLG1MjFXthY2Hr7K9kpYjxxIk6xmM7mFQE3Hts3bj6UU7ElUvHpX9bxxk3pqzlhg== MIIC6DCCAdCgAwIBAgIQaztYF2TpvZZG6yreA3NRpzANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVBREZTIFNpZ25pbmcgLSBmcy5yaWNoYXJkLXJhbmRhbGwuY29tMB4XDTEzMTExMTAzNTMwMFoXDTE0MTExMTAzNTMwMFowMDEuMCwGA1UEAxMlQURGUyBTaWduaW5nIC0gZnMucmljaGFyZC1yYW5kYWxsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO+1VWY/sYDdN3hdsvT+mWHTcOwjp2G9e0AEZdmgh7bS54WUJw9y0cMxJmGB0jAAW40zomzIbS8/o3iuxcJyFgBVtMFfXwFjVQJnZJ7IMXFs1V/pJHrwWHxePz/WzXFtMaqEIe8QummJ07UBg9UsYZUYTGO9NDGw1Yr/oRNsl7bLA0S/QlW6yryf6l3snHzIgtO2xiWn6q3vCJTTVNMROkI2YKNKdYiD5fFD77kFACfJmOwP8MN9u+HM2IN6g0Nv5s7rMyw077Co/xKefamWQCB0jLpv89jo3hLgkwIgWX4cMVgHSNmdzXSgC3owG8ivRuJDATh83GiqI6jzA1+x4rkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAxA5MQZHw9lJYDpU4f45EYrWPEaAPnncaoxIeLE9fG14gA01frajRfdyoO0AKqb+ZG6sePKngsuq4QHA2EnEI4Di5uWKsXy1Id0AXUSUhLpe63alZ8OwiNKDKn71nwpXnlGwKqljnG3xBMniGtGKrFS4WM+joEHzaKpvgtGRGoDdtXF4UXZJcn2maw6d/kiHrQ3kWoQcQcJ9hVIo8bC0BPvxV0Qh4TF3Nb3tKhaXsY68eMxMGbHok9trVHQ3Vew35FuTg1JzsfCFSDF8sxu7FJ4iZ7VLM8MQLnvIMcubLJvc57EHSsNyeiqBFQIYkdg7MSf+Ot2qJjfExgo+NOtWN+g== _9bd2b280-f153-471a-9b73-c1df0d555075 _9bd2b280-f153-471a-9b73-c1df0d555075 urn:oasis:names:tc:SAML:1.0:assertion http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer microsoft-authentication-library-for-python-1.1.0/tests/test_application.py000066400000000000000000000274621361242362500273750ustar00rootroot00000000000000# Note: Since Aug 2019 we move all e2e tests into test_e2e.py, # so this test_application file contains only unit tests without dependency. import os import json import logging try: from unittest.mock import * # Python 3 except: from mock import * # Need an external mock package from msal.application import * import msal from tests import unittest from tests.test_token_cache import TokenCacheTestCase logger = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) class TestHelperExtractCerts(unittest.TestCase): # It is used by SNI scenario def test_extract_a_tag_less_public_cert(self): pem = "my_cert" self.assertEqual(["my_cert"], extract_certs(pem)) def test_extract_a_tag_enclosed_cert(self): pem = """ -----BEGIN CERTIFICATE----- my_cert -----END CERTIFICATE----- """ self.assertEqual(["my_cert"], extract_certs(pem)) def test_extract_multiple_tag_enclosed_certs(self): pem = """ -----BEGIN CERTIFICATE----- my_cert1 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- my_cert2 -----END CERTIFICATE----- """ self.assertEqual(["my_cert1", "my_cert2"], extract_certs(pem)) class TestClientApplicationAcquireTokenSilentErrorBehaviors(unittest.TestCase): def setUp(self): self.authority_url = "https://login.microsoftonline.com/common" self.authority = msal.authority.Authority(self.authority_url) self.scopes = ["s1", "s2"] self.uid = "my_uid" self.utid = "my_utid" self.account = {"home_account_id": "{}.{}".format(self.uid, self.utid)} self.rt = "this is a rt" self.cache = msal.SerializableTokenCache() self.client_id = "my_app" self.cache.add({ # Pre-populate the cache "client_id": self.client_id, "scope": self.scopes, "token_endpoint": "{}/oauth2/v2.0/token".format(self.authority_url), "response": TokenCacheTestCase.build_response( access_token="an expired AT to trigger refresh", expires_in=-99, uid=self.uid, utid=self.utid, refresh_token=self.rt), }) # The add(...) helper populates correct home_account_id for future searching self.app = ClientApplication( self.client_id, authority=self.authority_url, token_cache=self.cache) def test_cache_empty_will_be_returned_as_None(self): self.assertEqual( None, self.app.acquire_token_silent(['cache_miss'], self.account)) self.assertEqual( None, self.app.acquire_token_silent_with_error(['cache_miss'], self.account)) def test_acquire_token_silent_will_suppress_error(self): error_response = {"error": "invalid_grant", "suberror": "xyz"} def tester(url, **kwargs): return Mock(status_code=400, json=Mock(return_value=error_response)) self.assertEqual(None, self.app.acquire_token_silent( self.scopes, self.account, post=tester)) def test_acquire_token_silent_with_error_will_return_error(self): error_response = {"error": "invalid_grant", "error_description": "xyz"} def tester(url, **kwargs): return Mock(status_code=400, json=Mock(return_value=error_response)) self.assertEqual(error_response, self.app.acquire_token_silent_with_error( self.scopes, self.account, post=tester)) def test_atswe_will_map_some_suberror_to_classification_as_is(self): error_response = {"error": "invalid_grant", "suberror": "basic_action"} def tester(url, **kwargs): return Mock(status_code=400, json=Mock(return_value=error_response)) result = self.app.acquire_token_silent_with_error( self.scopes, self.account, post=tester) self.assertEqual("basic_action", result.get("classification")) def test_atswe_will_map_some_suberror_to_classification_to_empty_string(self): error_response = {"error": "invalid_grant", "suberror": "client_mismatch"} def tester(url, **kwargs): return Mock(status_code=400, json=Mock(return_value=error_response)) result = self.app.acquire_token_silent_with_error( self.scopes, self.account, post=tester) self.assertEqual("", result.get("classification")) class TestClientApplicationAcquireTokenSilentFociBehaviors(unittest.TestCase): def setUp(self): self.authority_url = "https://login.microsoftonline.com/common" self.authority = msal.authority.Authority(self.authority_url) self.scopes = ["s1", "s2"] self.uid = "my_uid" self.utid = "my_utid" self.account = {"home_account_id": "{}.{}".format(self.uid, self.utid)} self.frt = "what the frt" self.cache = msal.SerializableTokenCache() self.preexisting_family_app_id = "preexisting_family_app" self.cache.add({ # Pre-populate a FRT "client_id": self.preexisting_family_app_id, "scope": self.scopes, "token_endpoint": "{}/oauth2/v2.0/token".format(self.authority_url), "response": TokenCacheTestCase.build_response( access_token="Siblings won't share AT. test_remove_account() will.", id_token=TokenCacheTestCase.build_id_token(aud=self.preexisting_family_app_id), uid=self.uid, utid=self.utid, refresh_token=self.frt, foci="1"), }) # The add(...) helper populates correct home_account_id for future searching def test_unknown_orphan_app_will_attempt_frt_and_not_remove_it(self): app = ClientApplication( "unknown_orphan", authority=self.authority_url, token_cache=self.cache) logger.debug("%s.cache = %s", self.id(), self.cache.serialize()) def tester(url, data=None, **kwargs): self.assertEqual(self.frt, data.get("refresh_token"), "Should attempt the FRT") return Mock(status_code=400, json=Mock(return_value={ "error": "invalid_grant", "error_description": "Was issued to another client"})) app._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family( self.authority, self.scopes, self.account, post=tester) self.assertNotEqual([], app.token_cache.find( msal.TokenCache.CredentialType.REFRESH_TOKEN, query={"secret": self.frt}), "The FRT should not be removed from the cache") def test_known_orphan_app_will_skip_frt_and_only_use_its_own_rt(self): app = ClientApplication( "known_orphan", authority=self.authority_url, token_cache=self.cache) rt = "RT for this orphan app. We will check it being used by this test case." self.cache.add({ # Populate its RT and AppMetadata, so it becomes a known orphan app "client_id": app.client_id, "scope": self.scopes, "token_endpoint": "{}/oauth2/v2.0/token".format(self.authority_url), "response": TokenCacheTestCase.build_response( uid=self.uid, utid=self.utid, refresh_token=rt), }) logger.debug("%s.cache = %s", self.id(), self.cache.serialize()) def tester(url, data=None, **kwargs): self.assertEqual(rt, data.get("refresh_token"), "Should attempt the RT") return Mock(status_code=200, json=Mock(return_value={})) app._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family( self.authority, self.scopes, self.account, post=tester) def test_unknown_family_app_will_attempt_frt_and_join_family(self): def tester(url, data=None, **kwargs): self.assertEqual( self.frt, data.get("refresh_token"), "Should attempt the FRT") return Mock( status_code=200, json=Mock(return_value=TokenCacheTestCase.build_response( uid=self.uid, utid=self.utid, foci="1", access_token="at"))) app = ClientApplication( "unknown_family_app", authority=self.authority_url, token_cache=self.cache) at = app._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family( self.authority, self.scopes, self.account, post=tester) logger.debug("%s.cache = %s", self.id(), self.cache.serialize()) self.assertEqual("at", at.get("access_token"), "New app should get a new AT") app_metadata = app.token_cache.find( msal.TokenCache.CredentialType.APP_METADATA, query={"client_id": app.client_id}) self.assertNotEqual([], app_metadata, "Should record new app's metadata") self.assertEqual("1", app_metadata[0].get("family_id"), "The new family app should be recorded as in the same family") # Known family app will simply use FRT, which is largely the same as this one # Will not test scenario of app leaving family. Per specs, it won't happen. def test_family_app_remove_account(self): logger.debug("%s.cache = %s", self.id(), self.cache.serialize()) app = ClientApplication( self.preexisting_family_app_id, authority=self.authority_url, token_cache=self.cache) account = app.get_accounts()[0] mine = {"home_account_id": account["home_account_id"]} self.assertNotEqual([], self.cache.find( self.cache.CredentialType.ACCESS_TOKEN, query=mine)) self.assertNotEqual([], self.cache.find( self.cache.CredentialType.REFRESH_TOKEN, query=mine)) self.assertNotEqual([], self.cache.find( self.cache.CredentialType.ID_TOKEN, query=mine)) self.assertNotEqual([], self.cache.find( self.cache.CredentialType.ACCOUNT, query=mine)) app.remove_account(account) self.assertEqual([], self.cache.find( self.cache.CredentialType.ACCESS_TOKEN, query=mine)) self.assertEqual([], self.cache.find( self.cache.CredentialType.REFRESH_TOKEN, query=mine)) self.assertEqual([], self.cache.find( self.cache.CredentialType.ID_TOKEN, query=mine)) self.assertEqual([], self.cache.find( self.cache.CredentialType.ACCOUNT, query=mine)) class TestClientApplicationForAuthorityMigration(unittest.TestCase): @classmethod def setUp(self): self.environment_in_cache = "sts.windows.net" self.authority_url_in_app = "https://login.microsoftonline.com/common" self.scopes = ["s1", "s2"] uid = "uid" utid = "utid" self.account = {"home_account_id": "{}.{}".format(uid, utid)} self.client_id = "my_app" self.access_token = "access token for testing authority aliases" self.cache = msal.SerializableTokenCache() self.cache.add({ "client_id": self.client_id, "scope": self.scopes, "token_endpoint": "https://{}/common/oauth2/v2.0/token".format( self.environment_in_cache), "response": TokenCacheTestCase.build_response( uid=uid, utid=utid, access_token=self.access_token, refresh_token="some refresh token"), }) # The add(...) helper populates correct home_account_id for future searching def test_get_accounts(self): app = ClientApplication( self.client_id, authority=self.authority_url_in_app, token_cache=self.cache) accounts = app.get_accounts() self.assertNotEqual([], accounts) self.assertEqual(self.environment_in_cache, accounts[0].get("environment"), "We should be able to find an account under an authority alias") def test_acquire_token_silent(self): app = ClientApplication( self.client_id, authority=self.authority_url_in_app, token_cache=self.cache) at = app.acquire_token_silent(self.scopes, self.account) self.assertNotEqual(None, at) self.assertEqual(self.access_token, at.get('access_token')) microsoft-authentication-library-for-python-1.1.0/tests/test_assertion.py000066400000000000000000000010101361242362500270560ustar00rootroot00000000000000import json from msal.oauth2cli import JwtAssertionCreator from msal.oauth2cli.oidc import decode_part from tests import unittest class AssertionTestCase(unittest.TestCase): def test_extra_claims(self): assertion = JwtAssertionCreator(key=None, algorithm="none").sign_assertion( "audience", "issuer", additional_claims={"client_ip": "1.2.3.4"}) payload = json.loads(decode_part(assertion.split(b'.')[1].decode('utf-8'))) self.assertEqual("1.2.3.4", payload.get("client_ip")) microsoft-authentication-library-for-python-1.1.0/tests/test_authority.py000066400000000000000000000104601361242362500271100ustar00rootroot00000000000000import os from msal.authority import * from msal.exceptions import MsalServiceError from tests import unittest @unittest.skipIf(os.getenv("TRAVIS_TAG"), "Skip network io during tagged release") class TestAuthority(unittest.TestCase): def test_wellknown_host_and_tenant(self): # Assert all well known authority hosts are using their own "common" tenant for host in WELL_KNOWN_AUTHORITY_HOSTS: a = Authority('https://{}/common'.format(host)) self.assertEqual( a.authorization_endpoint, 'https://%s/common/oauth2/v2.0/authorize' % host) self.assertEqual( a.token_endpoint, 'https://%s/common/oauth2/v2.0/token' % host) @unittest.skip("As of Jan 2017, the server no longer returns V1 endpoint") def test_lessknown_host_will_return_a_set_of_v1_endpoints(self): # This is an observation for current (2016-10) server-side behavior. # It is probably not a strict API contract. I simply mention it here. less_known = 'login.windows.net' # less.known.host/ v1_token_endpoint = 'https://{}/common/oauth2/token'.format(less_known) a = Authority('https://{}/common'.format(less_known)) self.assertEqual(a.token_endpoint, v1_token_endpoint) self.assertNotIn('v2.0', a.token_endpoint) def test_unknown_host_wont_pass_instance_discovery(self): _assert = getattr(self, "assertRaisesRegex", self.assertRaisesRegexp) # Hack with _assert(ValueError, "invalid_instance"): Authority('https://example.com/tenant_doesnt_matter_in_this_case') def test_invalid_host_skipping_validation_can_be_turned_off(self): try: Authority('https://example.com/invalid', validate_authority=False) except ValueError as e: if "invalid_instance" in str(e): # Imprecise but good enough self.fail("validate_authority=False should turn off validation") except: # Could be requests...RequestException, json...JSONDecodeError, etc. pass # Those are expected for this unittest case class TestAuthorityInternalHelperCanonicalize(unittest.TestCase): def test_canonicalize_tenant_followed_by_extra_paths(self): _, i, t = canonicalize("https://example.com/tenant/subpath?foo=bar#fragment") self.assertEqual("example.com", i) self.assertEqual("tenant", t) def test_canonicalize_tenant_followed_by_extra_query(self): _, i, t = canonicalize("https://example.com/tenant?foo=bar#fragment") self.assertEqual("example.com", i) self.assertEqual("tenant", t) def test_canonicalize_tenant_followed_by_extra_fragment(self): _, i, t = canonicalize("https://example.com/tenant#fragment") self.assertEqual("example.com", i) self.assertEqual("tenant", t) def test_canonicalize_rejects_non_https(self): with self.assertRaises(ValueError): canonicalize("http://non.https.example.com/tenant") def test_canonicalize_rejects_tenantless(self): with self.assertRaises(ValueError): canonicalize("https://no.tenant.example.com") def test_canonicalize_rejects_tenantless_host_with_trailing_slash(self): with self.assertRaises(ValueError): canonicalize("https://no.tenant.example.com/") @unittest.skipIf(os.getenv("TRAVIS_TAG"), "Skip network io during tagged release") class TestAuthorityInternalHelperUserRealmDiscovery(unittest.TestCase): def test_memorize(self): # We use a real authority so the constructor can finish tenant discovery authority = "https://login.microsoftonline.com/common" self.assertNotIn(authority, Authority._domains_without_user_realm_discovery) a = Authority(authority, validate_authority=False) # We now pretend this authority supports no User Realm Discovery class MockResponse(object): status_code = 404 a.user_realm_discovery("john.doe@example.com", response=MockResponse()) self.assertIn( "login.microsoftonline.com", Authority._domains_without_user_realm_discovery, "user_realm_discovery() should memorize domains not supporting URD") a.user_realm_discovery("john.doe@example.com", response="This would cause exception if memorization did not work") microsoft-authentication-library-for-python-1.1.0/tests/test_client.py000066400000000000000000000155521361242362500263450ustar00rootroot00000000000000import os import json import logging try: # Python 2 from urlparse import urljoin except: # Python 3 from urllib.parse import urljoin import time import requests from msal.oauth2cli import Client, JwtSigner from msal.oauth2cli.authcode import obtain_auth_code from tests import unittest, Oauth2TestCase logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__file__) CONFIG_FILENAME = "config.json" def load_conf(filename): """ Example of a configuration file: { "Note": "the OpenID Discovery will be updated by following optional content", "additional_openid_configuration": { "authorization_endpoint": "https://example.com/tenant/oauth2/authorize", "token_endpoint": "https://example.com/tenant/oauth2/token", "device_authorization_endpoint": "device_authorization" }, "client_id": "289a413d-284b-4303-9c79-94380abe5d22", "client_secret": "your_secret", "scope": ["your_scope"], "resource": "Some IdP needs this", "oidp": "https://example.com/tenant/", "username": "you@example.com", "password": "I could tell you but then I would have to kill you", "placeholder": null } """ conf = {} if os.path.exists(filename): with open(filename) as f: conf = json.load(f) else: # Do not raise unittest.SkipTest(...) here, # because it would still be considered as Test Error in Python 2 logger.warning("Unable to locate JSON configuration %s" % filename) openid_configuration = {} if "oidp" in conf: try: # The following line may duplicate a '/' at the joining point, # but requests.get(...) would still work. # Besides, standard urljoin(...) is picky on insisting oidp ends with '/' discovery_uri = conf["oidp"] + '/.well-known/openid-configuration' openid_configuration.update(requests.get(discovery_uri).json()) except: logger.warning( "openid configuration uri not accesible: %s", discovery_uri) openid_configuration.update(conf.get("additional_openid_configuration", {})) if openid_configuration.get("device_authorization_endpoint"): # The following urljoin(..., ...) trick allows a "path_name" shorthand openid_configuration["device_authorization_endpoint"] = urljoin( openid_configuration.get("token_endpoint", ""), openid_configuration.get("device_authorization_endpoint", "")) conf["openid_configuration"] = openid_configuration return conf THIS_FOLDER = os.path.dirname(__file__) CONFIG = load_conf(os.path.join(THIS_FOLDER, CONFIG_FILENAME)) or {} # Since the OAuth2 specs uses snake_case, this test config also uses snake_case @unittest.skipUnless("client_id" in CONFIG, "client_id missing") @unittest.skipUnless(CONFIG.get("openid_configuration"), "openid_configuration missing") class TestClient(Oauth2TestCase): @classmethod def setUpClass(cls): if "client_certificate" in CONFIG: private_key_path = CONFIG["client_certificate"]["private_key_path"] with open(os.path.join(THIS_FOLDER, private_key_path)) as f: private_key = f.read() # Expecting PEM format cls.client = Client( CONFIG["openid_configuration"], CONFIG['client_id'], client_assertion=JwtSigner( private_key, algorithm="RS256", sha1_thumbprint=CONFIG["client_certificate"]["thumbprint"] ).sign_assertion( audience=CONFIG["openid_configuration"]["token_endpoint"], issuer=CONFIG["client_id"], ), client_assertion_type=Client.CLIENT_ASSERTION_TYPE_JWT, ) else: cls.client = Client( CONFIG["openid_configuration"], CONFIG['client_id'], client_secret=CONFIG.get('client_secret')) @unittest.skipIf( "token_endpoint" not in CONFIG.get("openid_configuration", {}), "token_endpoint missing") @unittest.skipIf("client_secret" not in CONFIG, "client_secret missing") def test_client_credentials(self): result = self.client.obtain_token_for_client(CONFIG.get('scope')) self.assertIn('access_token', result) @unittest.skipIf( "token_endpoint" not in CONFIG.get("openid_configuration", {}), "token_endpoint missing") @unittest.skipIf( not ("username" in CONFIG and "password" in CONFIG), "username/password missing") def test_username_password(self): result = self.client.obtain_token_by_username_password( CONFIG["username"], CONFIG["password"], data={"resource": CONFIG.get("resource")}, # MSFT AAD V1 only scope=CONFIG.get("scope")) self.assertLoosely(result) @unittest.skipUnless( "authorization_endpoint" in CONFIG.get("openid_configuration", {}), "authorization_endpoint missing") def test_auth_code(self): port = CONFIG.get("listen_port", 44331) redirect_uri = "http://localhost:%s" % port auth_request_uri = self.client.build_auth_request_uri( "code", redirect_uri=redirect_uri, scope=CONFIG.get("scope")) ac = obtain_auth_code(port, auth_uri=auth_request_uri) self.assertNotEqual(ac, None) result = self.client.obtain_token_by_authorization_code( ac, data={ "scope": CONFIG.get("scope"), "resource": CONFIG.get("resource"), }, # MSFT AAD only redirect_uri=redirect_uri) self.assertLoosely(result, lambda: self.assertIn('access_token', result)) @unittest.skipUnless( CONFIG.get("openid_configuration", {}).get("device_authorization_endpoint"), "device_authorization_endpoint is missing") def test_device_flow(self): flow = self.client.initiate_device_flow(scope=CONFIG.get("scope")) try: msg = ("Use a web browser to open the page {verification_uri} and " "enter the code {user_code} to authenticate.".format(**flow)) except KeyError: # Some IdP might not be standard compliant msg = flow["message"] # Not a standard parameter though logger.warning(msg) # Avoid print(...) b/c its output would be buffered duration = 30 logger.warning("We will wait up to %d seconds for you to sign in" % duration) flow["expires_at"] = time.time() + duration # Shorten the time for quick test result = self.client.obtain_token_by_device_flow(flow) self.assertLoosely( result, assertion=lambda: self.assertIn('access_token', result), skippable_errors=self.client.DEVICE_FLOW_RETRIABLE_ERRORS) microsoft-authentication-library-for-python-1.1.0/tests/test_e2e.py000066400000000000000000000615251361242362500255430ustar00rootroot00000000000000import logging import os import json import time import unittest import requests import msal logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) def _get_app_and_auth_code( client_id, client_secret=None, authority="https://login.microsoftonline.com/common", port=44331, scopes=["https://graph.microsoft.com/.default"], # Microsoft Graph ): from msal.oauth2cli.authcode import obtain_auth_code app = msal.ClientApplication(client_id, client_secret, authority=authority) redirect_uri = "http://localhost:%d" % port ac = obtain_auth_code(port, auth_uri=app.get_authorization_request_url( scopes, redirect_uri=redirect_uri)) assert ac is not None return (app, ac, redirect_uri) @unittest.skipIf(os.getenv("TRAVIS_TAG"), "Skip e2e tests during tagged release") class E2eTestCase(unittest.TestCase): def assertLoosely(self, response, assertion=None, skippable_errors=("invalid_grant", "interaction_required")): if response.get("error") in skippable_errors: logger.debug("Response = %s", response) # Some of these errors are configuration issues, not library issues raise unittest.SkipTest(response.get("error_description")) else: if assertion is None: assertion = lambda: self.assertIn( "access_token", response, "{error}: {error_description}".format( # Do explicit response.get(...) rather than **response error=response.get("error"), error_description=response.get("error_description"))) assertion() def assertCacheWorksForUser(self, result_from_wire, scope, username=None): # You can filter by predefined username, or let end user to choose one accounts = self.app.get_accounts(username=username) self.assertNotEqual(0, len(accounts)) account = accounts[0] if ("scope" not in result_from_wire # This is the usual case or # Authority server could reject some scopes set(scope) <= set(result_from_wire["scope"].split(" ")) ): # Going to test acquire_token_silent(...) to locate an AT from cache result_from_cache = self.app.acquire_token_silent(scope, account=account) self.assertIsNotNone(result_from_cache) self.assertIsNone( result_from_cache.get("refresh_token"), "A cache hit returns no RT") self.assertEqual( result_from_wire['access_token'], result_from_cache['access_token'], "We should get a cached AT") # Going to test acquire_token_silent(...) to obtain an AT by a RT from cache self.app.token_cache._cache["AccessToken"] = {} # A hacky way to clear ATs result_from_cache = self.app.acquire_token_silent(scope, account=account) self.assertIsNotNone(result_from_cache, "We should get a result from acquire_token_silent(...) call") self.assertIsNotNone( # We used to assert it this way: # result_from_wire['access_token'] != result_from_cache['access_token'] # but ROPC in B2C tends to return the same AT we obtained seconds ago. # Now looking back, "refresh_token grant would return a brand new AT" # was just an empirical observation but never a committment in specs, # so we adjust our way to assert here. (result_from_cache or {}).get("access_token"), "We should get an AT from acquire_token_silent(...) call") def assertCacheWorksForApp(self, result_from_wire, scope): # Going to test acquire_token_silent(...) to locate an AT from cache result_from_cache = self.app.acquire_token_silent(scope, account=None) self.assertIsNotNone(result_from_cache) self.assertEqual( result_from_wire['access_token'], result_from_cache['access_token'], "We should get a cached AT") def _test_username_password(self, authority=None, client_id=None, username=None, password=None, scope=None, **ignored): assert authority and client_id and username and password and scope self.app = msal.PublicClientApplication(client_id, authority=authority) result = self.app.acquire_token_by_username_password( username, password, scopes=scope) self.assertLoosely(result) # self.assertEqual(None, result.get("error"), str(result)) self.assertCacheWorksForUser( result, scope, username=username if ".b2clogin.com" not in authority else None, ) THIS_FOLDER = os.path.dirname(__file__) CONFIG = os.path.join(THIS_FOLDER, "config.json") @unittest.skipUnless(os.path.exists(CONFIG), "Optional %s not found" % CONFIG) class FileBasedTestCase(E2eTestCase): # This covers scenarios that are not currently available for test automation. # So they mean to be run on maintainer's machine for semi-automated tests. @classmethod def setUpClass(cls): with open(CONFIG) as f: cls.config = json.load(f) def skipUnlessWithConfig(self, fields): for field in fields: if field not in self.config: self.skipTest('Skipping due to lack of configuration "%s"' % field) def test_username_password(self): self.skipUnlessWithConfig(["client_id", "username", "password", "scope"]) self._test_username_password(**self.config) def _get_app_and_auth_code(self): return _get_app_and_auth_code( self.config["client_id"], client_secret=self.config.get("client_secret"), authority=self.config.get("authority"), port=self.config.get("listen_port", 44331), scopes=self.config["scope"], ) def test_auth_code(self): self.skipUnlessWithConfig(["client_id", "scope"]) (self.app, ac, redirect_uri) = self._get_app_and_auth_code() result = self.app.acquire_token_by_authorization_code( ac, self.config["scope"], redirect_uri=redirect_uri) logger.debug("%s.cache = %s", self.id(), json.dumps(self.app.token_cache._cache, indent=4)) self.assertIn( "access_token", result, "{error}: {error_description}".format( # Note: No interpolation here, cause error won't always present error=result.get("error"), error_description=result.get("error_description"))) self.assertCacheWorksForUser(result, self.config["scope"], username=None) def test_ssh_cert(self): self.skipUnlessWithConfig(["client_id", "scope"]) JWK1 = """{"kty":"RSA", "n":"2tNr73xwcj6lH7bqRZrFzgSLj7OeLfbn8216uOMDHuaZ6TEUBDN8Uz0ve8jAlKsP9CQFCSVoSNovdE-fs7c15MxEGHjDcNKLWonznximj8pDGZQjVdfK-7mG6P6z-lgVcLuYu5JcWU_PeEqIKg5llOaz-qeQ4LEDS4T1D2qWRGpAra4rJX1-kmrWmX_XIamq30C9EIO0gGuT4rc2hJBWQ-4-FnE1NXmy125wfT3NdotAJGq5lMIfhjfglDbJCwhc8Oe17ORjO3FsB5CLuBRpYmP7Nzn66lRY3Fe11Xz8AEBl3anKFSJcTvlMnFtu3EpD-eiaHfTgRBU7CztGQqVbiQ", "e":"AQAB"}""" JWK2 = """{"kty":"RSA", "n":"72u07mew8rw-ssw3tUs9clKstGO2lvD7ZNxJU7OPNKz5PGYx3gjkhUmtNah4I4FP0DuF1ogb_qSS5eD86w10Wb1ftjWcoY8zjNO9V3ph-Q2tMQWdDW5kLdeU3-EDzc0HQeou9E0udqmfQoPbuXFQcOkdcbh3eeYejs8sWn3TQprXRwGh_TRYi-CAurXXLxQ8rp-pltUVRIr1B63fXmXhMeCAGwCPEFX9FRRs-YHUszUJl9F9-E0nmdOitiAkKfCC9LhwB9_xKtjmHUM9VaEC9jWOcdvXZutwEoW2XPMOg0Ky-s197F9rfpgHle2gBrXsbvVMvS0D-wXg6vsq6BAHzQ", "e":"AQAB"}""" data1 = {"token_type": "ssh-cert", "key_id": "key1", "req_cnf": JWK1} ssh_test_slice = { "dc": "prod-wst-test1", "slice": "test", "sshcrt": "true", } (self.app, ac, redirect_uri) = self._get_app_and_auth_code() result = self.app.acquire_token_by_authorization_code( ac, self.config["scope"], redirect_uri=redirect_uri, data=data1, params=ssh_test_slice) self.assertEqual("ssh-cert", result["token_type"]) logger.debug("%s.cache = %s", self.id(), json.dumps(self.app.token_cache._cache, indent=4)) # acquire_token_silent() needs to be passed the same key to work account = self.app.get_accounts()[0] result_from_cache = self.app.acquire_token_silent( self.config["scope"], account=account, data=data1) self.assertIsNotNone(result_from_cache) self.assertEqual( result['access_token'], result_from_cache['access_token'], "We should get the cached SSH-cert") # refresh_token grant can fetch an ssh-cert bound to a different key refreshed_ssh_cert = self.app.acquire_token_silent( self.config["scope"], account=account, params=ssh_test_slice, data={"token_type": "ssh-cert", "key_id": "key2", "req_cnf": JWK2}) self.assertIsNotNone(refreshed_ssh_cert) self.assertEqual(refreshed_ssh_cert["token_type"], "ssh-cert") self.assertNotEqual(result["access_token"], refreshed_ssh_cert['access_token']) def test_client_secret(self): self.skipUnlessWithConfig(["client_id", "client_secret"]) self.app = msal.ConfidentialClientApplication( self.config["client_id"], client_credential=self.config.get("client_secret"), authority=self.config.get("authority")) scope = self.config.get("scope", []) result = self.app.acquire_token_for_client(scope) self.assertIn('access_token', result) self.assertCacheWorksForApp(result, scope) def test_client_certificate(self): self.skipUnlessWithConfig(["client_id", "client_certificate"]) client_cert = self.config["client_certificate"] assert "private_key_path" in client_cert and "thumbprint" in client_cert with open(os.path.join(THIS_FOLDER, client_cert['private_key_path'])) as f: private_key = f.read() # Should be in PEM format self.app = msal.ConfidentialClientApplication( self.config['client_id'], {"private_key": private_key, "thumbprint": client_cert["thumbprint"]}) scope = self.config.get("scope", []) result = self.app.acquire_token_for_client(scope) self.assertIn('access_token', result) self.assertCacheWorksForApp(result, scope) def test_subject_name_issuer_authentication(self): self.skipUnlessWithConfig(["client_id", "client_certificate"]) client_cert = self.config["client_certificate"] assert "private_key_path" in client_cert and "thumbprint" in client_cert if not "public_certificate" in client_cert: self.skipTest("Skipping SNI test due to lack of public_certificate") with open(os.path.join(THIS_FOLDER, client_cert['private_key_path'])) as f: private_key = f.read() # Should be in PEM format with open(os.path.join(THIS_FOLDER, client_cert['public_certificate'])) as f: public_certificate = f.read() self.app = msal.ConfidentialClientApplication( self.config['client_id'], authority=self.config["authority"], client_credential={ "private_key": private_key, "thumbprint": self.config["thumbprint"], "public_certificate": public_certificate, }) scope = self.config.get("scope", []) result = self.app.acquire_token_for_client(scope) self.assertIn('access_token', result) self.assertCacheWorksForApp(result, scope) @unittest.skipUnless(os.path.exists(CONFIG), "Optional %s not found" % CONFIG) class DeviceFlowTestCase(E2eTestCase): # A leaf class so it will be run only once @classmethod def setUpClass(cls): with open(CONFIG) as f: cls.config = json.load(f) def test_device_flow(self): scopes = self.config["scope"] self.app = msal.PublicClientApplication( self.config['client_id'], authority=self.config["authority"]) flow = self.app.initiate_device_flow(scopes=scopes) assert "user_code" in flow, "DF does not seem to be provisioned: %s".format( json.dumps(flow, indent=4)) logger.info(flow["message"]) duration = 60 logger.info("We will wait up to %d seconds for you to sign in" % duration) flow["expires_at"] = min( # Shorten the time for quick test flow["expires_at"], time.time() + duration) result = self.app.acquire_token_by_device_flow(flow) self.assertLoosely( # It will skip this test if there is no user interaction result, assertion=lambda: self.assertIn('access_token', result), skippable_errors=self.app.client.DEVICE_FLOW_RETRIABLE_ERRORS) if "access_token" not in result: self.skip("End user did not complete Device Flow in time") self.assertCacheWorksForUser(result, scopes, username=None) result["access_token"] = result["refresh_token"] = "************" logger.info( "%s obtained tokens: %s", self.id(), json.dumps(result, indent=4)) def get_lab_app( env_client_id="LAB_APP_CLIENT_ID", env_client_secret="LAB_APP_CLIENT_SECRET", ): """Returns the lab app as an MSAL confidential client. Get it from environment variables if defined, otherwise fall back to use MSI. """ if os.getenv(env_client_id) and os.getenv(env_client_secret): # A shortcut mainly for running tests on developer's local development machine # or it could be setup on Travis CI # https://docs.travis-ci.com/user/environment-variables/#defining-variables-in-repository-settings # Data came from here # https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/Rese.aspx#programmatic-access-info-for-lab-request-api logger.info("Using lab app defined by ENV variables %s and %s", env_client_id, env_client_secret) client_id = os.getenv(env_client_id) client_secret = os.getenv(env_client_secret) else: logger.info("ENV variables %s and/or %s are not defined. Fall back to MSI.", env_client_id, env_client_secret) # See also https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/Programmatically-accessing-LAB-API's.aspx raise unittest.SkipTest("MSI-based mechanism has not been implemented yet") return msal.ConfidentialClientApplication(client_id, client_secret, authority="https://login.microsoftonline.com/" "72f988bf-86f1-41af-91ab-2d7cd011db47", # Microsoft tenant ID ) def get_session(lab_app, scopes): # BTW, this infrastructure tests the confidential client flow logger.info("Creating session") lab_token = lab_app.acquire_token_for_client(scopes) session = requests.Session() session.headers.update({"Authorization": "Bearer %s" % lab_token["access_token"]}) session.hooks["response"].append(lambda r, *args, **kwargs: r.raise_for_status()) return session class LabBasedTestCase(E2eTestCase): _secrets = {} adfs2019_scopes = ["placeholder"] # Need this to satisfy MSAL API surface. # Internally, MSAL will also append more scopes like "openid" etc.. # ADFS 2019 will issue tokens for valid scope only, by default "openid". # https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-faq#what-permitted-scopes-are-supported-by-ad-fs @classmethod def setUpClass(cls): # https://docs.msidlab.com/accounts/apiaccess.html#code-snippet cls.session = get_session(get_lab_app(), ["https://msidlab.com/.default"]) @classmethod def tearDownClass(cls): cls.session.close() @classmethod def get_lab_user_secret(cls, lab_name="msidlab4"): lab_name = lab_name.lower() if lab_name not in cls._secrets: logger.info("Querying lab user password for %s", lab_name) url = "https://msidlab.com/api/LabUserSecret?secret=%s" % lab_name resp = cls.session.get(url) cls._secrets[lab_name] = resp.json()["value"] return cls._secrets[lab_name] @classmethod def get_lab_user(cls, **query): # https://docs.msidlab.com/labapi/userapi.html resp = cls.session.get("https://msidlab.com/api/user", params=query) result = resp.json()[0] return { # Mapping lab API response to our simplified configuration format "authority": "https://login.microsoftonline.com/{}.onmicrosoft.com".format( result["labName"]), "client_id": result["appId"], "username": result["upn"], "lab_name": result["labName"], "scope": ["https://graph.microsoft.com/.default"], } def test_aad_managed_user(self): # Pure cloud config = self.get_lab_user(usertype="cloud") self._test_username_password( password=self.get_lab_user_secret(config["lab_name"]), **config) def test_adfs4_fed_user(self): config = self.get_lab_user(usertype="federated", federationProvider="ADFSv4") self._test_username_password( password=self.get_lab_user_secret(config["lab_name"]), **config) def test_adfs3_fed_user(self): config = self.get_lab_user(usertype="federated", federationProvider="ADFSv3") self._test_username_password( password=self.get_lab_user_secret(config["lab_name"]), **config) def test_adfs2_fed_user(self): config = self.get_lab_user(usertype="federated", federationProvider="ADFSv2") self._test_username_password( password=self.get_lab_user_secret(config["lab_name"]), **config) def test_adfs2019_fed_user(self): config = self.get_lab_user(usertype="federated", federationProvider="ADFSv2019") self._test_username_password( password=self.get_lab_user_secret(config["lab_name"]), **config) def test_ropc_adfs2019_onprem(self): config = self.get_lab_user(usertype="onprem", federationProvider="ADFSv2019") config["authority"] = "https://fs.%s.com/adfs" % config["lab_name"] config["client_id"] = "PublicClientId" config["scope"] = self.adfs2019_scopes self._test_username_password( password=self.get_lab_user_secret(config["lab_name"]), **config) @unittest.skipIf(os.getenv("TRAVIS"), "Browser automation is not yet implemented") def test_adfs2019_onprem_acquire_token_by_auth_code(self): """When prompted, you can manually login using this account: # https://msidlab.com/api/user?usertype=onprem&federationprovider=ADFSv2019 username = "..." # The upn from the link above password="***" # From https://aka.ms/GetLabUserSecret?Secret=msidlabXYZ """ scopes = self.adfs2019_scopes config = self.get_lab_user(usertype="onprem", federationProvider="ADFSv2019") (self.app, ac, redirect_uri) = _get_app_and_auth_code( # Configuration is derived from https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/4.7.0/tests/Microsoft.Identity.Test.Common/TestConstants.cs#L250-L259 "PublicClientId", authority="https://fs.%s.com/adfs" % config["lab_name"], port=8080, scopes=scopes, ) result = self.app.acquire_token_by_authorization_code( ac, scopes, redirect_uri=redirect_uri) logger.debug( "%s: cache = %s, id_token_claims = %s", self.id(), json.dumps(self.app.token_cache._cache, indent=4), json.dumps(result.get("id_token_claims"), indent=4), ) self.assertIn( "access_token", result, "{error}: {error_description}".format( # Note: No interpolation here, cause error won't always present error=result.get("error"), error_description=result.get("error_description"))) self.assertCacheWorksForUser(result, scopes, username=None) @unittest.skipUnless( os.getenv("OBO_CLIENT_SECRET"), "Need OBO_CLIENT_SECRET from https://buildautomation.vault.azure.net/secrets/IdentityDivisionDotNetOBOServiceSecret") def test_acquire_token_obo(self): # Some hardcoded, pre-defined settings obo_client_id = "23c64cd8-21e4-41dd-9756-ab9e2c23f58c" downstream_scopes = ["https://graph.microsoft.com/User.Read"] config = self.get_lab_user(usertype="cloud") # 1. An app obtains a token representing a user, for our mid-tier service pca = msal.PublicClientApplication( "be9b0186-7dfd-448a-a944-f771029105bf", authority=config.get("authority")) pca_result = pca.acquire_token_by_username_password( config["username"], self.get_lab_user_secret(config["lab_name"]), scopes=[ # The OBO app's scope. Yours might be different. "%s/access_as_user" % obo_client_id], ) self.assertIsNotNone( pca_result.get("access_token"), "PCA failed to get AT because %s" % json.dumps(pca_result, indent=2)) # 2. Our mid-tier service uses OBO to obtain a token for downstream service cca = msal.ConfidentialClientApplication( obo_client_id, client_credential=os.getenv("OBO_CLIENT_SECRET"), authority=config.get("authority"), # token_cache= ..., # Default token cache is all-tokens-store-in-memory. # That's fine if OBO app uses short-lived msal instance per session. # Otherwise, the OBO app need to implement a one-cache-per-user setup. ) cca_result = cca.acquire_token_on_behalf_of( pca_result['access_token'], downstream_scopes) self.assertNotEqual(None, cca_result.get("access_token"), str(cca_result)) # 3. Now the OBO app can simply store downstream token(s) in same session. # Alternatively, if you want to persist the downstream AT, and possibly # the RT (if any) for prolonged access even after your own AT expires, # now it is the time to persist current cache state for current user. # Assuming you already did that (which is not shown in this test case), # the following part shows one of the ways to obtain an AT from cache. username = cca_result.get("id_token_claims", {}).get("preferred_username") self.assertEqual(config["username"], username) if username: # A precaution so that we won't use other user's token account = cca.get_accounts(username=username)[0] result = cca.acquire_token_silent(downstream_scopes, account) self.assertEqual(cca_result["access_token"], result["access_token"]) def _build_b2c_authority(self, policy): base = "https://msidlabb2c.b2clogin.com/msidlabb2c.onmicrosoft.com" return base + "/" + policy # We do not support base + "?p=" + policy @unittest.skipIf(os.getenv("TRAVIS"), "Browser automation is not yet implemented") def test_b2c_acquire_token_by_auth_code(self): """ When prompted, you can manually login using this account: username="b2clocal@msidlabb2c.onmicrosoft.com" # This won't work https://msidlab.com/api/user?usertype=b2c password="***" # From https://aka.ms/GetLabUserSecret?Secret=msidlabb2c """ scopes = ["https://msidlabb2c.onmicrosoft.com/msaapp/user_impersonation"] (self.app, ac, redirect_uri) = _get_app_and_auth_code( "b876a048-55a5-4fc5-9403-f5d90cb1c852", client_secret=self.get_lab_user_secret("MSIDLABB2C-MSAapp-AppSecret"), authority=self._build_b2c_authority("B2C_1_SignInPolicy"), port=3843, # Lab defines 4 of them: [3843, 4584, 4843, 60000] scopes=scopes, ) result = self.app.acquire_token_by_authorization_code( ac, scopes, redirect_uri=redirect_uri) logger.debug( "%s: cache = %s, id_token_claims = %s", self.id(), json.dumps(self.app.token_cache._cache, indent=4), json.dumps(result.get("id_token_claims"), indent=4), ) self.assertIn( "access_token", result, "{error}: {error_description}".format( # Note: No interpolation here, cause error won't always present error=result.get("error"), error_description=result.get("error_description"))) self.assertCacheWorksForUser(result, scopes, username=None) def test_b2c_acquire_token_by_ropc(self): self._test_username_password( authority=self._build_b2c_authority("B2C_1_ROPC_Auth"), client_id="e3b9ad76-9763-4827-b088-80c7a7888f79", username="b2clocal@msidlabb2c.onmicrosoft.com", password=self.get_lab_user_secret("msidlabb2c"), scope=["https://msidlabb2c.onmicrosoft.com/msidlabb2capi/read"], ) if __name__ == "__main__": unittest.main() microsoft-authentication-library-for-python-1.1.0/tests/test_mex.py000066400000000000000000000016471361242362500256600ustar00rootroot00000000000000import os from tests import unittest from msal.mex import * THIS_FOLDER = os.path.dirname(__file__) class TestMex(unittest.TestCase): def _test_parser(self, sample, expected_endpoint): with open(os.path.join(THIS_FOLDER, sample)) as sample_file: endpoint = Mex(mex_document=sample_file.read() ).get_wstrust_username_password_endpoint()["address"] self.assertEqual(expected_endpoint, endpoint) def test_happy_path_1(self): self._test_parser("microsoft.mex.xml", 'https://corp.sts.microsoft.com/adfs/services/trust/13/usernamemixed') def test_happy_path_2(self): self._test_parser('arupela.mex.xml', 'https://fs.arupela.com/adfs/services/trust/13/usernamemixed') def test_happy_path_3(self): self._test_parser('archan.us.mex.xml', 'https://arvmserver2012.archan.us/adfs/services/trust/13/usernamemixed') microsoft-authentication-library-for-python-1.1.0/tests/test_token_cache.py000066400000000000000000000232201361242362500273210ustar00rootroot00000000000000import logging import base64 import json import time from msal.token_cache import * from tests import unittest logger = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) class TokenCacheTestCase(unittest.TestCase): @staticmethod def build_id_token( iss="issuer", sub="subject", aud="my_client_id", exp=None, iat=None, **claims): # AAD issues "preferred_username", ADFS issues "upn" return "header.%s.signature" % base64.b64encode(json.dumps(dict({ "iss": iss, "sub": sub, "aud": aud, "exp": exp or (time.time() + 100), "iat": iat or time.time(), }, **claims)).encode()).decode('utf-8') @staticmethod def build_response( # simulate a response from AAD uid=None, utid=None, # If present, they will form client_info access_token=None, expires_in=3600, token_type="some type", refresh_token=None, foci=None, id_token=None, # or something generated by build_id_token() error=None, ): response = {} if uid and utid: # Mimic the AAD behavior for "client_info=1" request response["client_info"] = base64.b64encode(json.dumps({ "uid": uid, "utid": utid, }).encode()).decode('utf-8') if error: response["error"] = error if access_token: response.update({ "access_token": access_token, "expires_in": expires_in, "token_type": token_type, }) if refresh_token: response["refresh_token"] = refresh_token if id_token: response["id_token"] = id_token if foci: response["foci"] = foci return response def setUp(self): self.cache = TokenCache() def testAddByAad(self): client_id = "my_client_id" id_token = self.build_id_token( oid="object1234", preferred_username="John Doe", aud=client_id) self.cache.add({ "client_id": client_id, "scope": ["s2", "s1", "s3"], # Not in particular order "token_endpoint": "https://login.example.com/contoso/v2/token", "response": self.build_response( uid="uid", utid="utid", # client_info expires_in=3600, access_token="an access token", id_token=id_token, refresh_token="a refresh token"), }, now=1000) self.assertEqual( { 'cached_at': "1000", 'client_id': 'my_client_id', 'credential_type': 'AccessToken', 'environment': 'login.example.com', 'expires_on': "4600", 'extended_expires_on': "4600", 'home_account_id': "uid.utid", 'realm': 'contoso', 'secret': 'an access token', 'target': 's2 s1 s3', 'token_type': 'some type', }, self.cache._cache["AccessToken"].get( 'uid.utid-login.example.com-accesstoken-my_client_id-contoso-s2 s1 s3') ) self.assertEqual( { 'client_id': 'my_client_id', 'credential_type': 'RefreshToken', 'environment': 'login.example.com', 'home_account_id': "uid.utid", 'secret': 'a refresh token', 'target': 's2 s1 s3', }, self.cache._cache["RefreshToken"].get( 'uid.utid-login.example.com-refreshtoken-my_client_id--s2 s1 s3') ) self.assertEqual( { 'home_account_id': "uid.utid", 'environment': 'login.example.com', 'realm': 'contoso', 'local_account_id': "object1234", 'username': "John Doe", 'authority_type': "MSSTS", }, self.cache._cache["Account"].get('uid.utid-login.example.com-contoso') ) self.assertEqual( { 'credential_type': 'IdToken', 'secret': id_token, 'home_account_id': "uid.utid", 'environment': 'login.example.com', 'realm': 'contoso', 'client_id': 'my_client_id', }, self.cache._cache["IdToken"].get( 'uid.utid-login.example.com-idtoken-my_client_id-contoso-') ) self.assertEqual( { "client_id": "my_client_id", 'environment': 'login.example.com', }, self.cache._cache.get("AppMetadata", {}).get( "appmetadata-login.example.com-my_client_id") ) def testAddByAdfs(self): client_id = "my_client_id" id_token = self.build_id_token(aud=client_id, upn="JaneDoe@example.com") self.cache.add({ "client_id": client_id, "scope": ["s2", "s1", "s3"], # Not in particular order "token_endpoint": "https://fs.msidlab8.com/adfs/oauth2/token", "response": self.build_response( uid=None, utid=None, # ADFS will provide no client_info expires_in=3600, access_token="an access token", id_token=id_token, refresh_token="a refresh token"), }, now=1000) self.assertEqual( { 'cached_at': "1000", 'client_id': 'my_client_id', 'credential_type': 'AccessToken', 'environment': 'fs.msidlab8.com', 'expires_on': "4600", 'extended_expires_on': "4600", 'home_account_id': "subject", 'realm': 'adfs', 'secret': 'an access token', 'target': 's2 s1 s3', 'token_type': 'some type', }, self.cache._cache["AccessToken"].get( 'subject-fs.msidlab8.com-accesstoken-my_client_id-adfs-s2 s1 s3') ) self.assertEqual( { 'client_id': 'my_client_id', 'credential_type': 'RefreshToken', 'environment': 'fs.msidlab8.com', 'home_account_id': "subject", 'secret': 'a refresh token', 'target': 's2 s1 s3', }, self.cache._cache["RefreshToken"].get( 'subject-fs.msidlab8.com-refreshtoken-my_client_id--s2 s1 s3') ) self.assertEqual( { 'home_account_id': "subject", 'environment': 'fs.msidlab8.com', 'realm': 'adfs', 'local_account_id': "subject", 'username': "JaneDoe@example.com", 'authority_type': "ADFS", }, self.cache._cache["Account"].get('subject-fs.msidlab8.com-adfs') ) self.assertEqual( { 'credential_type': 'IdToken', 'secret': id_token, 'home_account_id': "subject", 'environment': 'fs.msidlab8.com', 'realm': 'adfs', 'client_id': 'my_client_id', }, self.cache._cache["IdToken"].get( 'subject-fs.msidlab8.com-idtoken-my_client_id-adfs-') ) self.assertEqual( { "client_id": "my_client_id", 'environment': 'fs.msidlab8.com', }, self.cache._cache.get("AppMetadata", {}).get( "appmetadata-fs.msidlab8.com-my_client_id") ) def test_key_id_is_also_recorded(self): my_key_id = "some_key_id_123" self.cache.add({ "data": {"key_id": my_key_id}, "client_id": "my_client_id", "scope": ["s2", "s1", "s3"], # Not in particular order "token_endpoint": "https://login.example.com/contoso/v2/token", "response": self.build_response( uid="uid", utid="utid", # client_info expires_in=3600, access_token="an access token", refresh_token="a refresh token"), }, now=1000) cached_key_id = self.cache._cache["AccessToken"].get( 'uid.utid-login.example.com-accesstoken-my_client_id-contoso-s2 s1 s3', {}).get("key_id") self.assertEqual(my_key_id, cached_key_id, "AT should be bound to the key") class SerializableTokenCacheTestCase(TokenCacheTestCase): # Run all inherited test methods, and have extra check in tearDown() def setUp(self): self.cache = SerializableTokenCache() self.cache.deserialize(""" { "AccessToken": { "an-entry": { "foo": "bar" } }, "customized": "whatever" } """) def test_has_state_changed(self): cache = SerializableTokenCache() self.assertFalse(cache.has_state_changed) cache.add({}) # An NO-OP add() still counts as a state change. Good enough. self.assertTrue(cache.has_state_changed) def tearDown(self): state = self.cache.serialize() logger.debug("serialize() = %s", state) # Now assert all extended content are kept intact output = json.loads(state) self.assertEqual(output.get("customized"), "whatever", "Undefined cache keys and their values should be intact") self.assertEqual( output.get("AccessToken", {}).get("an-entry"), {"foo": "bar"}, "Undefined token keys and their values should be intact") microsoft-authentication-library-for-python-1.1.0/tests/test_wstrust.py000066400000000000000000000104041361242362500266110ustar00rootroot00000000000000#------------------------------------------------------------------------------ # # Copyright (c) Microsoft Corporation. # All rights reserved. # # This code is licensed under the MIT License. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files(the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions : # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #------------------------------------------------------------------------------ try: from xml.etree import cElementTree as ET except ImportError: from xml.etree import ElementTree as ET import os from msal.wstrust_response import * from tests import unittest class Test_WsTrustResponse(unittest.TestCase): def test_findall_content_with_comparison(self): content = """ foo """ sample = ('' + content + '') # Demonstrating how XML-based parser won't give you the raw content as-is element = ET.fromstring(sample).findall('{SAML:assertion}Assertion')[0] assertion_via_xml_parser = ET.tostring(element) self.assertNotEqual(content, assertion_via_xml_parser) self.assertNotIn(b"", assertion_via_xml_parser) # The findall_content() helper, based on Regex, will return content as-is. self.assertEqual([content], findall_content(sample, "Wrapper")) def test_parse_error(self): error_response = ''' http://www.w3.org/2005/08/addressing/soap/fault 2013-07-30T00:32:21.989Z 2013-07-30T00:37:21.989Z s:Sender a:RequestFailed MSIS3127: The specified request failed. ''' self.assertEqual({ "reason": "MSIS3127: The specified request failed.", "code": "a:RequestFailed", }, parse_error(error_response)) def test_token_parsing_happy_path(self): with open(os.path.join(os.path.dirname(__file__), "rst_response.xml")) as f: rst_body = f.read() result = parse_token_by_re(rst_body) self.assertEqual(result.get("type"), SAML_TOKEN_TYPE_V1) self.assertIn(b"