Hello community, here is the log from the commit of package python-msal for openSUSE:Factory checked in at 2020-06-17 14:50:58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-msal (Old) and /work/SRC/openSUSE:Factory/.python-msal.new.3606 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-msal" Wed Jun 17 14:50:58 2020 rev:2 rq:815262 version:1.3.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-msal/python-msal.changes 2020-02-28 15:19:57.949725105 +0100 +++ /work/SRC/openSUSE:Factory/.python-msal.new.3606/python-msal.changes 2020-06-17 14:51:17.369176790 +0200 @@ -1,0 +2,28 @@ +Tue Jun 16 13:11:05 UTC 2020 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- Update to version 1.3.0 + + New feature: class ```ClientApplication``` accepts a new optional parameter + ```http_client```. You can provide your own HTTP client to have different + behavior. (#169) Please refer to API Reference doc. + + New feature: method ```get_authorization_request_url()``` accepts a new optional + parameter ```domain_hint```. (#158, #181) + Please refer to API Reference doc. + + New feature: A new method ```acquire_token_by_refresh_token()``` to help migrating + refresh tokens from elsewhere to MSAL Python. (#193) + Its usage is demonstrated in this sample. +- from version 1.2.0 + + New ```nonce``` parameter is provided in ```both get_authorization_request_url(..., nonce=...)``` + and ```acquire_token_by_authorization_code(..., nonce=...)``` method, so + that you can use them to mitigate replay attacks, per OIDC specs. (#128, #173). +- from version 1.1.0 + + New ```acquire_token_silent_with_error(...)``` method to expose conditional + access error classifications (#143, closes #57). + + App developers can opt in to provide their app's name and version for Microsoft + Telemetry, so that we can understand your usage pattern and serve you better. + (#136 closes #130) + + Internally, + * Collect anonymous telemetry data to help us improve MSAL Python (#103) + * Test cases cover ADFS 2019 on-premise scenarios (#142, closes #132) + * Switched to our latest lab apis for better test infrastructure (#108, #133, #134, #135) + +------------------------------------------------------------------- Old: ---- msal-1.1.0.tar.gz New: ---- msal-1.3.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-msal.spec ++++++ --- /var/tmp/diff_new_pack.Na3XMR/_old 2020-06-17 14:51:18.209179667 +0200 +++ /var/tmp/diff_new_pack.Na3XMR/_new 2020-06-17 14:51:18.209179667 +0200 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-msal -Version: 1.1.0 +Version: 1.3.0 Release: 0 Summary: Microsoft Authentication Library (MSAL) for Python License: MIT ++++++ msal-1.1.0.tar.gz -> msal-1.3.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.1.0/PKG-INFO new/msal-1.3.0/PKG-INFO --- old/msal-1.1.0/PKG-INFO 2020-01-24 00:12:24.000000000 +0100 +++ new/msal-1.3.0/PKG-INFO 2020-05-15 05:35:49.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: msal -Version: 1.1.0 +Version: 1.3.0 Summary: The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect. Home-page: https://github.com/AzureAD/microsoft-authentication-library-for-python Author: Microsoft Corporation @@ -9,9 +9,9 @@ Description: # 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) + | `dev` branch | Reference Docs | # of Downloads + |---------------|---------------|----------------| + [![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) | [![Download monthly](https://pepy.tech/badge/msal/month)](https://pypistats.org/packages/msal) 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 @@ -43,6 +43,11 @@ [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. + (Note: That is the high level conceptual pattern. + There will be some variations for different flows. They are demonstrated in + [runnable samples hosted right in this repo](https://github.com/AzureAD/microsoft-authentication-library-for-python/tree/dev/sample). + ) + 1. MSAL proposes a clean separation between [public client applications, and confidential client applications](https://tools.ietf.org/html/rfc6749#section-2.1). @@ -51,7 +56,9 @@ ```python from msal import PublicClientApplication - app = PublicClientApplication("your_client_id", authority="...") + app = PublicClientApplication( + "your_client_id", + "authority": "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here") ``` Later, each time you would want an access token, you start by: @@ -75,7 +82,7 @@ # 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) + result = app.acquire_token_silent(["your_scope"], account=chosen) ``` 3. Either there is no suitable token in the cache, or you chose to skip the previous step, @@ -94,9 +101,6 @@ 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.1.0/README.md new/msal-1.3.0/README.md --- old/msal-1.1.0/README.md 2020-01-24 00:11:59.000000000 +0100 +++ new/msal-1.3.0/README.md 2020-05-15 05:35:24.000000000 +0200 @@ -1,9 +1,9 @@ # 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) +| `dev` branch | Reference Docs | # of Downloads +|---------------|---------------|----------------| + [![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) | [![Download monthly](https://pepy.tech/badge/msal/month)](https://pypistats.org/packages/msal) 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 @@ -35,6 +35,11 @@ [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. +(Note: That is the high level conceptual pattern. +There will be some variations for different flows. They are demonstrated in +[runnable samples hosted right in this repo](https://github.com/AzureAD/microsoft-authentication-library-for-python/tree/dev/sample). +) + 1. MSAL proposes a clean separation between [public client applications, and confidential client applications](https://tools.ietf.org/html/rfc6749#section-2.1). @@ -43,7 +48,9 @@ ```python from msal import PublicClientApplication - app = PublicClientApplication("your_client_id", authority="...") + app = PublicClientApplication( + "your_client_id", + "authority": "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here") ``` Later, each time you would want an access token, you start by: @@ -67,7 +74,7 @@ # 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) + result = app.acquire_token_silent(["your_scope"], account=chosen) ``` 3. Either there is no suitable token in the cache, or you chose to skip the previous step, @@ -86,9 +93,6 @@ 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.1.0/msal/application.py new/msal-1.3.0/msal/application.py --- old/msal-1.1.0/msal/application.py 2020-01-24 00:11:59.000000000 +0100 +++ new/msal-1.3.0/msal/application.py 2020-05-15 05:35:24.000000000 +0200 @@ -1,3 +1,5 @@ +import functools +import json import time try: # Python 2 from urlparse import urljoin @@ -19,7 +21,7 @@ # The __init__.py will import this. Not the other way around. -__version__ = "1.1.0" +__version__ = "1.3.0" logger = logging.getLogger(__name__) @@ -54,11 +56,11 @@ CLIENT_CURRENT_TELEMETRY = 'x-client-current-telemetry' def _get_new_correlation_id(): - return str(uuid.uuid4()) + 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") + return "1|{},{}|".format(public_api_id, "1" if force_refresh else "0") def extract_certs(public_cert_content): @@ -92,12 +94,14 @@ self, client_id, client_credential=None, authority=None, validate_authority=True, token_cache=None, + http_client=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: + :param str client_id: Your app has a client_id after you register it on AAD. + + :param str client_credential: For :class:`PublicClientApplication`, you simply use `None` here. For :class:`ConfidentialClientApplication`, it can be a string containing client secret, @@ -114,6 +118,17 @@ which will be sent through 'x5c' JWT header only for subject name and issuer authentication to support cert auto rolls. + Per `specs <https://tools.ietf.org/html/rfc7515#section-4.1.6>`_, + "the certificate containing + the public key corresponding to the key used to digitally sign the + JWS MUST be the first certificate. This MAY be followed by + additional certificates, with each subsequent certificate being the + one used to certify the previous one." + However, your certificate's issuer may use a different order. + So, if your attempt ends up with an error AADSTS700027 - + "The provided signature value did not match the expected signature value", + you may try use only the leaf cert (in PEM/str format) instead. + :param dict client_claims: *Added in version 0.5.0*: It is a dictionary of extra claims that would be signed by @@ -139,18 +154,24 @@ :param TokenCache cache: Sets the token cache used by this ClientApplication instance. By default, an in-memory cache will be created and used. + :param http_client: (optional) + Your implementation of abstract class HttpClient <msal.oauth2cli.http.http_client> + Defaults to a requests session instance :param verify: (optional) It will be passed to the `verify parameter in the underlying requests library <http://docs.python-requests.org/en/v2.9.1/user/advanced/#ssl-cert-verification>`_ + This does not apply if you have chosen to pass your own Http client :param proxies: (optional) It will be passed to the `proxies parameter in the underlying requests library <http://docs.python-requests.org/en/v2.9.1/user/advanced/#proxies>`_ + This does not apply if you have chosen to pass your own Http client :param timeout: (optional) It will be passed to the `timeout parameter in the underlying requests library <http://docs.python-requests.org/en/v2.9.1/user/advanced/#timeouts>`_ + This does not apply if you have chosen to pass your own Http client :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. @@ -161,14 +182,21 @@ self.client_id = client_id self.client_credential = client_credential self.client_claims = client_claims - self.verify = verify - self.proxies = proxies - self.timeout = timeout + if http_client: + self.http_client = http_client + else: + self.http_client = requests.Session() + self.http_client.verify = verify + self.http_client.proxies = proxies + # Requests, does not support session - wide timeout + # But you can patch that (https://github.com/psf/requests/issues/3341): + self.http_client.request = functools.partial( + self.http_client.request, 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) + self.http_client, validate_authority=validate_authority) # 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) @@ -211,14 +239,14 @@ return Client( server_configuration, self.client_id, + http_client=self.http_client, 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) + on_updating_rt=self.token_cache.update_rt) def get_authorization_request_url( self, @@ -229,6 +257,8 @@ redirect_uri=None, response_type="code", # Can be "token" if you use Implicit Grant prompt=None, + nonce=None, + domain_hint=None, # type: Optional[str] **kwargs): """Constructs a URL for you to start a Authorization Code Grant. @@ -247,6 +277,15 @@ 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 + :param nonce: + A cryptographically random value used to mitigate replay attacks. See also + `OIDC specs <https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest>`_. + :param domain_hint: + Can be one of "consumers" or "organizations" or your tenant domain "contoso.com". + If included, it will skip the email-based discovery process that user goes + through on the sign-in page, leading to a slightly more streamlined user experience. + https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapx/86fb452d-e34a-494e-ac61-e526e263b6d8 :return: The authorization url as a string. """ """ # TBD: this would only be meaningful in a new acquire_token_interactive() @@ -265,17 +304,20 @@ # Multi-tenant app can use new authority on demand the_authority = Authority( authority, - verify=self.verify, proxies=self.proxies, timeout=self.timeout, + self.http_client ) if authority else self.authority client = Client( {"authorization_endpoint": the_authority.authorization_endpoint}, - self.client_id) + self.client_id, + http_client=self.http_client) 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), + nonce=nonce, + domain_hint=domain_hint, ) def acquire_token_by_authorization_code( @@ -286,6 +328,7 @@ # 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. + nonce=None, **kwargs): """The second half of the Authorization Code Grant. @@ -306,6 +349,11 @@ So the developer need to specify a scope so that we can restrict the token to be issued for the corresponding audience. + :param nonce: + If you provided a nonce when calling :func:`get_authorization_request_url`, + same nonce should also be provided here, so that we'll validate it. + An exception will be raised if the nonce in id token mismatches. + :return: A dict representing the json response from AAD: - A successful response would contain "access_token" key, @@ -326,6 +374,7 @@ CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID), }, + nonce=nonce, **kwargs) def get_accounts(self, username=None): @@ -367,13 +416,12 @@ def _get_authority_aliases(self, instance): if not self.authority_groups: - resp = requests.get( + resp = self.http_client.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) + headers={'Accept': 'application/json'}) resp.raise_for_status() self.authority_groups = [ - set(group['aliases']) for group in resp.json()['metadata']] + set(group['aliases']) for group in json.loads(resp.text)['metadata']] for group in self.authority_groups: if instance in group: return [alias for alias in group if alias != instance] @@ -492,7 +540,7 @@ 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, + # self.http_client, # ) if authority else self.authority result = self._acquire_token_silent_from_cache_and_possibly_refresh_it( scopes, account, self.authority, force_refresh=force_refresh, @@ -504,8 +552,8 @@ 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) + self.http_client, + validate_authority=False) result = self._acquire_token_silent_from_cache_and_possibly_refresh_it( scopes, account, the_authority, force_refresh=force_refresh, correlation_id=correlation_id, @@ -585,8 +633,9 @@ **kwargs) if at and "error" not in at: return at + last_resp = None if app_metadata.get("family_id"): # Meaning this app belongs to this family - at = self._acquire_token_silent_by_finding_specific_refresh_token( + last_resp = 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: @@ -594,7 +643,8 @@ # 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) + authority, scopes, dict(query, client_id=self.client_id), + **kwargs) or last_resp def _get_app_metadata(self, environment): apps = self.token_cache.find( # Use find(), rather than token_cache.get(...) @@ -650,6 +700,36 @@ "you must include a string parameter named 'key_id' " "which identifies the key in the 'req_cnf' argument.") + def acquire_token_by_refresh_token(self, refresh_token, scopes): + """Acquire token(s) based on a refresh token (RT) obtained from elsewhere. + + You use this method only when you have old RTs from elsewhere, + and now you want to migrate them into MSAL. + Calling this method results in new tokens automatically storing into MSAL. + + You do NOT need to use this method if you are already using MSAL. + MSAL maintains RT automatically inside its token cache, + and an access token can be retrieved + when you call :func:`~acquire_token_silent`. + + :param str refresh_token: The old refresh token, as a string. + + :param list scopes: + The scopes associate with this old RT. + Each scope needs to be in the Microsoft identity platform (v2) format. + See `Scopes not resources <https://docs.microsoft.com/en-us/azure/active-directory/develop/migrate-python-adal-msal#scopes-not-resources>`_. + + :return: + * A dict contains "error" and some other keys, when error happened. + * A dict contains no "error" key means migration was successful. + """ + return self.client.obtain_token_by_refresh_token( + refresh_token, + decorate_scope(scopes, self.client_id), + rt_getter=lambda rt: rt, + on_updating_rt=False, + ) + class PublicClientApplication(ClientApplication): # browser app or mobile app @@ -713,7 +793,7 @@ def acquire_token_by_username_password( self, username, password, scopes, **kwargs): - """Gets a token for a given resource via user credentails. + """Gets a token for a given resource via user credentials. See this page for constraints of Username Password Flow. https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Username-Password-Authentication @@ -748,13 +828,11 @@ 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) + self.http_client) if wstrust_endpoint is None: raise ValueError("Unable to find wstrust endpoint from MEX. " "This typically happens when attempting MSA accounts. " @@ -766,7 +844,7 @@ 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) + wstrust_endpoint.get("action"), self.http_client) 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' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.1.0/msal/authority.py new/msal-1.3.0/msal/authority.py --- old/msal-1.1.0/msal/authority.py 2020-01-24 00:11:59.000000000 +0100 +++ new/msal-1.3.0/msal/authority.py 2020-05-15 05:35:24.000000000 +0200 @@ -1,10 +1,14 @@ +import json try: from urllib.parse import urlparse except ImportError: # Fall back to Python 2 from urlparse import urlparse import logging +# Historically some customers patched this module-wide requests instance. +# We keep it here for now. They will be removed in next major release. import requests +import requests as _requests from .exceptions import MsalServiceError @@ -25,6 +29,7 @@ "b2clogin.de", ] + class Authority(object): """This class represents an (already-validated) authority. @@ -33,9 +38,12 @@ """ _domains_without_user_realm_discovery = set([]) - def __init__(self, authority_url, validate_authority=True, - verify=True, proxies=None, timeout=None, - ): + @property + def http_client(self): # Obsolete. We will remove this in next major release. + # A workaround: if module-wide requests is patched, we honor it. + return self._http_client if requests is _requests else requests + + def __init__(self, authority_url, http_client, validate_authority=True): """Creates an authority instance, and also validates it. :param validate_authority: @@ -44,9 +52,7 @@ This parameter only controls whether an instance discovery will be performed. """ - self.verify = verify - self.proxies = proxies - self.timeout = timeout + self._http_client = http_client 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 ( @@ -56,7 +62,7 @@ payload = instance_discovery( "https://{}{}/oauth2/v2.0/authorize".format( self.instance, authority.path), - verify=verify, proxies=proxies, timeout=timeout) + self.http_client) if payload.get("error") == "invalid_instance": raise ValueError( "invalid_instance: " @@ -73,9 +79,16 @@ 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) + try: + openid_config = tenant_discovery( + tenant_discovery_endpoint, + self.http_client) + except ValueError: # json.decoder.JSONDecodeError in Py3 subclasses this + raise ValueError( + "Unable to get authority configuration for {}. " + "Authority would typically be in a format of " + "https://login.microsoftonline.com/your_tenant_name".format( + authority_url)) logger.debug("openid_config = %s", openid_config) self.authorization_endpoint = openid_config['authorization_endpoint'] self.token_endpoint = openid_config['token_endpoint'] @@ -87,15 +100,14 @@ # "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( + resp = response or self.http_client.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) + headers={'Accept': 'application/json', + 'client-request-id': correlation_id},) if resp.status_code != 404: resp.raise_for_status() - return resp.json() + return json.loads(resp.text) self.__class__._domains_without_user_realm_discovery.add(self.instance) return {} # This can guide the caller to fall back normal ROPC flow @@ -113,20 +125,21 @@ % authority_url) return authority, authority.hostname, parts[1] -def instance_discovery(url, **kwargs): - return requests.get( # Note: This URL seemingly returns V1 endpoint only +def instance_discovery(url, http_client, **kwargs): + resp = http_client.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() + **kwargs) + return json.loads(resp.text) -def tenant_discovery(tenant_discovery_endpoint, **kwargs): +def tenant_discovery(tenant_discovery_endpoint, http_client, **kwargs): # Returns Openid Configuration - resp = requests.get(tenant_discovery_endpoint, **kwargs) - payload = resp.json() + resp = http_client.get(tenant_discovery_endpoint, **kwargs) + payload = json.loads(resp.text) if 'authorization_endpoint' in payload and 'token_endpoint' in payload: return payload raise MsalServiceError(status_code=resp.status_code, **payload) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.1.0/msal/mex.py new/msal-1.3.0/msal/mex.py --- old/msal-1.1.0/msal/mex.py 2020-01-24 00:11:59.000000000 +0100 +++ new/msal-1.3.0/msal/mex.py 2020-05-15 05:35:24.000000000 +0200 @@ -34,15 +34,14 @@ 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( + +def send_request(mex_endpoint, http_client, **kwargs): + mex_document = http_client.get( mex_endpoint, headers={'Content-Type': 'application/soap+xml'}, **kwargs).text return Mex(mex_document).get_wstrust_username_password_endpoint() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.1.0/msal/oauth2cli/http.py new/msal-1.3.0/msal/oauth2cli/http.py --- old/msal-1.1.0/msal/oauth2cli/http.py 1970-01-01 01:00:00.000000000 +0100 +++ new/msal-1.3.0/msal/oauth2cli/http.py 2020-05-15 05:35:24.000000000 +0200 @@ -0,0 +1,69 @@ +"""This module documents the minimal http behaviors used by this package. + +Its interface is influenced by, and similar to a subset of some popular, +real-world http libraries, such as requests, aiohttp and httpx. +""" + + +class HttpClient(object): + """This describes a minimal http request interface used by this package.""" + + def post(self, url, params=None, data=None, headers=None, **kwargs): + """HTTP post. + + :param dict params: A dict to be url-encoded and sent as query-string. + :param dict headers: A dict representing headers to be sent via request. + :param data: + Implementation needs to support 2 types. + + * A dict, which will need to be urlencode() before being sent. + * (Recommended) A string, which will be sent in request as-is. + + It returns an :class:`~Response`-like object. + + Note: In its async counterpart, this method would be defined as async. + """ + return Response() + + def get(self, url, params=None, headers=None, **kwargs): + """HTTP get. + + :param dict params: A dict to be url-encoded and sent as query-string. + :param dict headers: A dict representing headers to be sent via request. + + It returns an :class:`~Response`-like object. + + Note: In its async counterpart, this method would be defined as async. + """ + return Response() + + +class Response(object): + """This describes a minimal http response interface used by this package. + + :var int status_code: + The status code of this http response. + + Our async code path would also accept an alias as "status". + + :var string text: + The body of this http response. + + Our async code path would also accept an awaitable with the same name. + """ + status_code = 200 # Our async code path would also accept a name as "status" + + text = "body as a string" # Our async code path would also accept an awaitable + # We could define a json() method instead of a text property/method, + # but a `text` would be more generic, + # when downstream packages would potentially access some XML endpoints. + + def raise_for_status(self): + """Raise an exception when http response status contains error""" + raise NotImplementedError("Your implementation should provide this") + + +def _get_status_code(resp): + # RFC defines and some libraries use "status_code", others use "status" + return getattr(resp, "status_code", None) or resp.status + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.1.0/msal/oauth2cli/oauth2.py new/msal-1.3.0/msal/oauth2cli/oauth2.py --- old/msal-1.1.0/msal/oauth2cli/oauth2.py 2020-01-24 00:11:59.000000000 +0100 +++ new/msal-1.3.0/msal/oauth2cli/oauth2.py 2020-05-15 05:35:24.000000000 +0200 @@ -1,6 +1,7 @@ """This OAuth2 client implementation aims to be spec-compliant, and generic.""" # OAuth2 spec https://tools.ietf.org/html/rfc6749 +import json try: from urllib.parse import urlencode, parse_qs except ImportError: @@ -11,6 +12,7 @@ import time import base64 import sys +import functools import requests @@ -31,16 +33,28 @@ CLIENT_ASSERTION_TYPE_SAML2 = "urn:ietf:params:oauth:client-assertion-type:saml2-bearer" client_assertion_encoders = {CLIENT_ASSERTION_TYPE_SAML2: encode_saml_assertion} + @property + def session(self): + warnings.warn("Will be gone in next major release", DeprecationWarning) + return self._http_client + + @session.setter + def session(self, value): + warnings.warn("Will be gone in next major release", DeprecationWarning) + self._http_client = value + + def __init__( self, server_configuration, # type: dict client_id, # type: str + http_client=None, # We insert it here to match the upcoming async API 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] + verify=None, # type: Union[str, True, False, None] proxies=None, # type: Optional[dict] timeout=None, # type: Union[tuple, float, None] ): @@ -57,6 +71,21 @@ or https://example.com/.../.well-known/openid-configuration client_id (str): The client's id, issued by the authorization server + + http_client (http.HttpClient): + Your implementation of abstract class :class:`http.HttpClient`. + Defaults to a requests session instance. + + There is no session-wide `timeout` parameter defined here. + Timeout behavior is determined by the actual http client you use. + If you happen to use Requests, it disallows session-wide timeout + (https://github.com/psf/requests/issues/3341). The workaround is: + + s = requests.Session() + s.request = functools.partial(s.request, timeout=3) + + and then feed that patched session instance to this class. + client_secret (str): Triggers HTTP AUTH for Confidential Client client_assertion (bytes, callable): The client assertion to authenticate this client, per RFC 7521. @@ -76,20 +105,52 @@ 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). + + verify (boolean): + It will be passed to the + `verify parameter in the underlying requests library + <http://docs.python-requests.org/en/v2.9.1/user/advanced/#ssl-cert-verification>`_. + When leaving it with default value (None), we will use True instead. + + This does not apply if you have chosen to pass your own Http client. + + proxies (dict): + It will be passed to the + `proxies parameter in the underlying requests library + <http://docs.python-requests.org/en/v2.9.1/user/advanced/#proxies>`_. + + This does not apply if you have chosen to pass your own Http client. + + timeout (object): + It will be passed to the + `timeout parameter in the underlying requests library + <http://docs.python-requests.org/en/v2.9.1/user/advanced/#timeouts>`_. + + This does not apply if you have chosen to pass your own Http client. + """ self.configuration = server_configuration self.client_id = client_id self.client_secret = client_secret self.client_assertion = client_assertion + self.default_headers = default_headers or {} 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 + if http_client: + if verify is not None or proxies is not None or timeout is not None: + raise ValueError( + "verify, proxies, or timeout is not allowed " + "when http_client is in use") + self._http_client = http_client + else: + self._http_client = requests.Session() + self._http_client.verify = True if verify is None else verify + self._http_client.proxies = proxies + self._http_client.request = functools.partial( + # A workaround for requests not supporting session-wide timeout + self._http_client.request, timeout=timeout) def _build_auth_request_params(self, response_type, **kwargs): # response_type is a string defined in @@ -110,10 +171,9 @@ 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={})) + # Mock(status_code=200, text='{}') **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} @@ -128,11 +188,15 @@ _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. + _data = {k: v for k, v in _data.items() if v} # Clean up None values if _data.get('scope'): _data['scope'] = self._stringify(_data['scope']) + _headers = {'Accept': 'application/json'} + _headers.update(self.default_headers) + _headers.update(headers or {}) + # 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. @@ -140,18 +204,16 @@ # 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 + _headers["Authorization"] = "Basic " + base64.b64encode( + "{}:{}".format(self.client_id, self.client_secret) + .encode("ascii")).decode("ascii") 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)( + resp = (post or self._http_client.post)( self.configuration["token_endpoint"], - headers=_headers, params=params, data=_data, auth=auth, - timeout=timeout or self.timeout, + headers=_headers, params=params, data=_data, **kwargs) if resp.status_code >= 500: resp.raise_for_status() # TODO: Will probably retry here @@ -159,7 +221,7 @@ # 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() + return json.loads(resp.text) except ValueError: self.logger.exception( "Token response is not in json format: %s", resp.text) @@ -200,7 +262,7 @@ grant_assertion_encoders = {GRANT_TYPE_SAML2: BaseClient.encode_saml_assertion} - def initiate_device_flow(self, scope=None, timeout=None, **kwargs): + def initiate_device_flow(self, scope=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 @@ -218,10 +280,11 @@ 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], + resp = self._http_client.post(self.configuration[DAE], data={"client_id": self.client_id, "scope": self._stringify(scope or [])}, - timeout=timeout or self.timeout, - **kwargs).json() + headers=dict(self.default_headers, **kwargs.pop("headers", {})), + **kwargs) + flow = json.loads(resp.text) 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 @@ -391,17 +454,20 @@ 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" + def _obtain_token( + self, grant_type, params=None, data=None, + also_save_rt=False, + *args, **kwargs): _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() + RT = "refresh_token" + if grant_type == RT and RT in _resp and not also_save_rt: + # Then we skip it from on_obtaining_tokens(); + # Leave it to self.obtain_token_by_refresh_token() + _resp.pop(RT, None) 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 @@ -423,6 +489,7 @@ def obtain_token_by_refresh_token(self, token_item, scope=None, rt_getter=lambda token_item: token_item["refresh_token"], on_removing_rt=None, + on_updating_rt=None, **kwargs): # type: (Union[str, dict], Union[str, list, set, tuple], Callable) -> dict """This is an overload which will trigger token storage callbacks. @@ -440,16 +507,28 @@ 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 + + :param on_updating_rt: + Default to None, it will fall back to the one defined in initialization. + This is the most common case. + + As a special case, you can pass in a False, + then this function will NOT trigger on_updating_rt() for RT UPDATE, + instead it will allow the RT to be added by on_obtaining_tokens(). + This behavior is useful when you are migrating RTs from elsewhere + into a token storage managed by this library. """ 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, + also_save_rt=on_updating_rt is False, **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']) + RT = "refresh_token" + if on_updating_rt is not False and RT in resp: + (on_updating_rt or self.on_updating_rt)(token_item, resp[RT]) return resp def obtain_token_by_assertion( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.1.0/msal/oauth2cli/oidc.py new/msal-1.3.0/msal/oauth2cli/oidc.py --- old/msal-1.1.0/msal/oauth2cli/oidc.py 2020-01-24 00:11:59.000000000 +0100 +++ new/msal-1.3.0/msal/oauth2cli/oidc.py 2020-05-15 05:35:24.000000000 +0200 @@ -85,3 +85,37 @@ ret["id_token_claims"] = self.decode_id_token(ret["id_token"]) return ret + def build_auth_request_uri(self, response_type, nonce=None, **kwargs): + """Generate an authorization uri to be visited by resource owner. + + Return value and all other parameters are the same as + :func:`oauth2.Client.build_auth_request_uri`, plus new parameter(s): + + :param nonce: + A hard-to-guess string used to mitigate replay attacks. See also + `OIDC specs <https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest>`_. + """ + return super(Client, self).build_auth_request_uri( + response_type, nonce=nonce, **kwargs) + + def obtain_token_by_authorization_code(self, code, nonce=None, **kwargs): + """Get a token via auhtorization code. a.k.a. Authorization Code Grant. + + Return value and all other parameters are the same as + :func:`oauth2.Client.obtain_token_by_authorization_code`, + plus new parameter(s): + + :param nonce: + If you provided a nonce when calling :func:`build_auth_request_uri`, + same nonce should also be provided here, so that we'll validate it. + An exception will be raised if the nonce in id token mismatches. + """ + result = super(Client, self).obtain_token_by_authorization_code( + code, **kwargs) + nonce_in_id_token = result.get("id_token_claims", {}).get("nonce") + if "id_token_claims" in result and nonce and nonce != nonce_in_id_token: + raise ValueError( + 'The nonce in id token ("%s") should match your nonce ("%s")' % + (nonce_in_id_token, nonce)) + return result + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.1.0/msal/wstrust_request.py new/msal-1.3.0/msal/wstrust_request.py --- old/msal-1.1.0/msal/wstrust_request.py 2020-01-24 00:11:59.000000000 +0100 +++ new/msal-1.3.0/msal/wstrust_request.py 2020-05-15 05:35:24.000000000 +0200 @@ -29,16 +29,13 @@ 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, + username, password, cloud_audience_urn, endpoint_address, soap_action, http_client, **kwargs): if not endpoint_address: raise ValueError("WsTrust endpoint address can not be empty") @@ -51,7 +48,7 @@ "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={ + resp = http_client.post(endpoint_address, data=data, headers={ 'Content-type':'application/soap+xml; charset=utf-8', 'SOAPAction': soap_action, }, **kwargs) @@ -61,11 +58,13 @@ # 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 @@ -74,6 +73,7 @@ # 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 """<s:Envelope xmlns:s='{s}' xmlns:wsa='{wsa}' xmlns:wsu='{wsu}'> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.1.0/msal.egg-info/PKG-INFO new/msal-1.3.0/msal.egg-info/PKG-INFO --- old/msal-1.1.0/msal.egg-info/PKG-INFO 2020-01-24 00:12:24.000000000 +0100 +++ new/msal-1.3.0/msal.egg-info/PKG-INFO 2020-05-15 05:35:49.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: msal -Version: 1.1.0 +Version: 1.3.0 Summary: The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect. Home-page: https://github.com/AzureAD/microsoft-authentication-library-for-python Author: Microsoft Corporation @@ -9,9 +9,9 @@ Description: # 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) + | `dev` branch | Reference Docs | # of Downloads + |---------------|---------------|----------------| + [![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) | [![Download monthly](https://pepy.tech/badge/msal/month)](https://pypistats.org/packages/msal) 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 @@ -43,6 +43,11 @@ [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. + (Note: That is the high level conceptual pattern. + There will be some variations for different flows. They are demonstrated in + [runnable samples hosted right in this repo](https://github.com/AzureAD/microsoft-authentication-library-for-python/tree/dev/sample). + ) + 1. MSAL proposes a clean separation between [public client applications, and confidential client applications](https://tools.ietf.org/html/rfc6749#section-2.1). @@ -51,7 +56,9 @@ ```python from msal import PublicClientApplication - app = PublicClientApplication("your_client_id", authority="...") + app = PublicClientApplication( + "your_client_id", + "authority": "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here") ``` Later, each time you would want an access token, you start by: @@ -75,7 +82,7 @@ # 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) + result = app.acquire_token_silent(["your_scope"], account=chosen) ``` 3. Either there is no suitable token in the cache, or you chose to skip the previous step, @@ -94,9 +101,6 @@ 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.1.0/msal.egg-info/SOURCES.txt new/msal-1.3.0/msal.egg-info/SOURCES.txt --- old/msal-1.1.0/msal.egg-info/SOURCES.txt 2020-01-24 00:12:24.000000000 +0100 +++ new/msal-1.3.0/msal.egg-info/SOURCES.txt 2020-05-15 05:35:49.000000000 +0200 @@ -17,5 +17,6 @@ msal/oauth2cli/__init__.py msal/oauth2cli/assertion.py msal/oauth2cli/authcode.py +msal/oauth2cli/http.py msal/oauth2cli/oauth2.py msal/oauth2cli/oidc.py \ No newline at end of file