Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-msal for openSUSE:Factory checked in at 2021-07-01 07:05:40 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-msal (Old) and /work/SRC/openSUSE:Factory/.python-msal.new.2625 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-msal" Thu Jul 1 07:05:40 2021 rev:8 rq:903198 version:1.12.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-msal/python-msal.changes 2021-04-12 15:49:43.449261928 +0200 +++ /work/SRC/openSUSE:Factory/.python-msal.new.2625/python-msal.changes 2021-07-01 07:05:55.447341313 +0200 @@ -1,0 +2,28 @@ +Tue Jun 29 12:40:07 UTC 2021 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- Update to version 1.12.0 + + New feature: MSAL Python supports ConfidentialClientApplication(..., azure_region=...). + If your app is deployed in Azure, you can use this new feature to pin a region. + (#295, #358) + + New feature: Historically MSAL Python attempts to acquire a Refresh Token (RT) by + default. Since this version, MSAL Python supports ConfidentialClientApplication(..., + excluse_scopes=["offline_access"]) to opt out of RT (#207, #361) + + Improvement: acquire_token_interactive(...) can also trigger browser when + running inside WSL (8d86917) + + Adjustment: get_accounts(...) would automatically combine equivalent accounts, + so that your account selector widget could be easier to use (#349) + + Document: MSAL Python has long been accepting acquire_token_interactive(..., prompt="create"), + now we officially documented it. (#356, #360) +- from version 1.11.0 + + Enhancement: ConfidentialClientApplication also supports + acquire_token_by_username_password() now. (#294, #344) + + Enhancement: PublicClientApplication's acquire_token_interactive() also supports WSL Ubuntu + 18.04 (#332, #333) + + Enhancement: Enable a retry once behavior on connection error. (But this is only available + from the default http client. If your app supplies your customized http_client via MSAL + constructors, it is your http_client's job to decide whether retry.) (#326) + + Enhancement: MSAL improves the internal telemetry mechanism. (#137, #175, #329, #345) + + Bugfix: Better compatibility on handling SAML token when using + acquire_token_by_username_password() with ADFS. (#336) + +------------------------------------------------------------------- Old: ---- msal-1.10.0.tar.gz New: ---- msal-1.12.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-msal.spec ++++++ --- /var/tmp/diff_new_pack.RSVMx7/_old 2021-07-01 07:05:55.879337938 +0200 +++ /var/tmp/diff_new_pack.RSVMx7/_new 2021-07-01 07:05:55.879337938 +0200 @@ -21,7 +21,7 @@ %define skip_python2 1 %endif Name: python-msal -Version: 1.10.0 +Version: 1.12.0 Release: 0 Summary: Microsoft Authentication Library (MSAL) for Python License: MIT ++++++ msal-1.10.0.tar.gz -> msal-1.12.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/LICENSE new/msal-1.12.0/LICENSE --- old/msal-1.10.0/LICENSE 1970-01-01 01:00:00.000000000 +0100 +++ new/msal-1.12.0/LICENSE 2021-05-19 22:28:05.000000000 +0200 @@ -0,0 +1,24 @@ +The 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. \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/PKG-INFO new/msal-1.12.0/PKG-INFO --- old/msal-1.10.0/PKG-INFO 2021-03-08 21:46:32.805985200 +0100 +++ new/msal-1.12.0/PKG-INFO 2021-05-19 22:28:13.778312400 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: msal -Version: 1.10.0 +Version: 1.12.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 @@ -21,8 +21,14 @@ 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) - | --- | --- | --- | --- | + | [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) | [Feedback](https://forms.office.com/r/TMjZkDbzjY) | + | --- | --- | --- | --- | --- | + + ## Scenarios supported + + Click on the following thumbnail to visit a large map with clickable links to proper samples. + + [](https://msal-python.readthedocs.io/en/latest/) ## Installation @@ -135,6 +141,9 @@ Here is the latest Q&A on Stack Overflow for MSAL: [http://stackoverflow.com/questions/tagged/msal](http://stackoverflow.com/questions/tagged/msal) + ## Submit Feedback + We'd like your thoughts on this library. Please complete [this short survey.](https://forms.office.com/r/TMjZkDbzjY) + ## Security Reporting If you find a security issue with our libraries or services please report it to [sec...@microsoft.com](mailto:sec...@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. @@ -153,11 +162,11 @@ Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.3 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Description-Content-Type: text/markdown diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/README.md new/msal-1.12.0/README.md --- old/msal-1.10.0/README.md 2021-03-08 21:46:19.000000000 +0100 +++ new/msal-1.12.0/README.md 2021-05-19 22:28:05.000000000 +0200 @@ -12,8 +12,14 @@ 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) -| --- | --- | --- | --- | +| [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) | [Feedback](https://forms.office.com/r/TMjZkDbzjY) | +| --- | --- | --- | --- | --- | + +## Scenarios supported + +Click on the following thumbnail to visit a large map with clickable links to proper samples. + +[](https://msal-python.readthedocs.io/en/latest/) ## Installation @@ -126,6 +132,9 @@ Here is the latest Q&A on Stack Overflow for MSAL: [http://stackoverflow.com/questions/tagged/msal](http://stackoverflow.com/questions/tagged/msal) +## Submit Feedback +We'd like your thoughts on this library. Please complete [this short survey.](https://forms.office.com/r/TMjZkDbzjY) + ## Security Reporting If you find a security issue with our libraries or services please report it to [sec...@microsoft.com](mailto:sec...@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. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/msal/__init__.py new/msal-1.12.0/msal/__init__.py --- old/msal-1.10.0/msal/__init__.py 2021-03-08 21:46:19.000000000 +0100 +++ new/msal-1.12.0/msal/__init__.py 2021-05-19 22:28:05.000000000 +0200 @@ -31,5 +31,6 @@ ConfidentialClientApplication, PublicClientApplication, ) +from .oauth2cli.oidc import Prompt from .token_cache import TokenCache, SerializableTokenCache diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/msal/application.py new/msal-1.12.0/msal/application.py --- old/msal-1.10.0/msal/application.py 2021-03-08 21:46:19.000000000 +0100 +++ new/msal-1.12.0/msal/application.py 2021-05-19 22:28:05.000000000 +0200 @@ -8,7 +8,7 @@ import logging import sys import warnings -import uuid +from threading import Lock import requests @@ -18,52 +18,15 @@ from .wstrust_request import send_request as wst_send_request from .wstrust_response import * from .token_cache import TokenCache +import msal.telemetry +from .region import _detect_region # The __init__.py will import this. Not the other way around. -__version__ = "1.10.0" +__version__ = "1.12.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(): - correlation_id = str(uuid.uuid4()) - logger.debug("Generates correlation_id: %s", correlation_id) - return correlation_id - - -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 @@ -119,6 +82,8 @@ GET_ACCOUNTS_ID = "902" REMOVE_ACCOUNT_ID = "903" + ATTEMPT_REGION_DISCOVERY = True # "TryAutoDetect" + def __init__( self, client_id, client_credential=None, authority=None, validate_authority=True, @@ -126,12 +91,18 @@ http_client=None, verify=True, proxies=None, timeout=None, client_claims=None, app_name=None, app_version=None, - client_capabilities=None): + client_capabilities=None, + azure_region=None, # Note: We choose to add this param in this base class, + # despite it is currently only needed by ConfidentialClientApplication. + # This way, it holds the same positional param place for PCA, + # when we would eventually want to add this feature to PCA in future. + exclude_scopes=None, + ): """Create an instance of application. :param str client_id: Your app has a client_id after you register it on AAD. - :param str client_credential: + :param Union[str, dict] client_credential: For :class:`PublicClientApplication`, you simply use `None` here. For :class:`ConfidentialClientApplication`, it can be a string containing client secret, @@ -187,7 +158,12 @@ 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 + Defaults to a requests session instance. + Since MSAL 1.11.0, the default session would be configured + to attempt one retry on connection error. + If you are providing your own http_client, + it will be your http_client's duty to decide whether to perform retry. + :param verify: (optional) It will be passed to the `verify parameter in the underlying requests library @@ -226,11 +202,75 @@ MSAL will combine them into `claims parameter <https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter`_ which you will later provide via one of the acquire-token request. + + :param str azure_region: + Added since MSAL Python 1.12.0. + + As of 2021 May, regional service is only available for + ``acquire_token_for_client()`` sent by any of the following scenarios:: + + 1. An app powered by a capable MSAL + (MSAL Python 1.12+ will be provisioned) + + 2. An app with managed identity, which is formerly known as MSI. + (However MSAL Python does not support managed identity, + so this one does not apply.) + + 3. An app authenticated by + `Subject Name/Issuer (SNI) <https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/60>`_. + + 4. An app which already onboard to the region's allow-list. + + MSAL's default value is None, which means region behavior remains off. + If enabled, the `acquire_token_for_client()`-relevant traffic + would remain inside that region. + + App developer can opt in to a regional endpoint, + by provide its region name, such as "westus", "eastus2". + You can find a full list of regions by running + ``az account list-locations -o table``, or referencing to + `this doc <https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.management.resourcemanager.fluent.core.region?view=azure-dotnet>`_. + + An app running inside Azure Functions and Azure VM can use a special keyword + ``ClientApplication.ATTEMPT_REGION_DISCOVERY`` to auto-detect region. + + .. note:: + + Setting ``azure_region`` to non-``None`` for an app running + outside of Azure Function/VM could hang indefinitely. + + You should consider opting in/out region behavior on-demand, + by loading ``azure_region=None`` or ``azure_region="westus"`` + or ``azure_region=True`` (which means opt-in and auto-detect) + from your per-deployment configuration, and then do + ``app = ConfidentialClientApplication(..., azure_region=azure_region)``. + + Alternatively, you can configure a short timeout, + or provide a custom http_client which has a short timeout. + That way, the latency would be under your control, + but still less performant than opting out of region feature. + :param list[str] exclude_scopes: (optional) + Historically MSAL hardcodes `offline_access` scope, + which would allow your app to have prolonged access to user's data. + If that is unnecessary or undesirable for your app, + now you can use this parameter to supply an exclusion list of scopes, + such as ``exclude_scopes = ["offline_access"]``. """ self.client_id = client_id self.client_credential = client_credential self.client_claims = client_claims self._client_capabilities = client_capabilities + + if exclude_scopes and not isinstance(exclude_scopes, list): + raise ValueError( + "Invalid exclude_scopes={}. It need to be a list of strings.".format( + repr(exclude_scopes))) + self._exclude_scopes = frozenset(exclude_scopes or []) + if "openid" in self._exclude_scopes: + raise ValueError( + 'Invalid exclude_scopes={}. You can not opt out "openid" scope'.format( + repr(exclude_scopes))) + if http_client: self.http_client = http_client else: @@ -241,15 +281,101 @@ # But you can patch that (https://github.com/psf/requests/issues/3341): self.http_client.request = functools.partial( self.http_client.request, timeout=timeout) + + # Enable a minimal retry. Better than nothing. + # https://github.com/psf/requests/blob/v2.25.1/requests/adapters.py#L94-L108 + a = requests.adapters.HTTPAdapter(max_retries=1) + self.http_client.mount("http://", a) + self.http_client.mount("https://", a) + self.app_name = app_name self.app_version = app_version - self.authority = Authority( + + # Here the self.authority will not be the same type as authority in input + try: + self.authority = Authority( authority or "https://login.microsoftonline.com/common/", self.http_client, validate_authority=validate_authority) - # Here the self.authority is not the same type as authority in input + except ValueError: # Those are explicit authority validation errors + raise + except Exception: # The rest are typically connection errors + if validate_authority and azure_region: + # Since caller opts in to use region, here we tolerate connection + # errors happened during authority validation at non-region endpoint + self.authority = Authority( + authority or "https://login.microsoftonline.com/common/", + self.http_client, validate_authority=False) + else: + raise + self.token_cache = token_cache or TokenCache() - self.client = self._build_client(client_credential, self.authority) + self._region_configured = azure_region + self._region_detected = None + self.client, self._regional_client = self._build_client( + client_credential, self.authority) self.authority_groups = None + self._telemetry_buffer = {} + self._telemetry_lock = Lock() + + def _decorate_scope( + self, scopes, + 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 self.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 + decorated -= self._exclude_scopes + return list(decorated) + + def _build_telemetry_context( + self, api_id, correlation_id=None, refresh_reason=None): + return msal.telemetry._TelemetryContext( + self._telemetry_buffer, self._telemetry_lock, api_id, + correlation_id=correlation_id, refresh_reason=refresh_reason) + + def _get_regional_authority(self, central_authority): + is_region_specified = bool(self._region_configured + and self._region_configured != self.ATTEMPT_REGION_DISCOVERY) + self._region_detected = self._region_detected or _detect_region( + self.http_client if self._region_configured is not None else None) + if (is_region_specified and self._region_configured != self._region_detected): + logger.warning('Region configured ({}) != region detected ({})'.format( + repr(self._region_configured), repr(self._region_detected))) + region_to_use = ( + self._region_configured if is_region_specified else self._region_detected) + if region_to_use: + logger.info('Region to be used: {}'.format(repr(region_to_use))) + regional_host = ("{}.login.microsoft.com".format(region_to_use) + if central_authority.instance in ( + # The list came from https://github.com/AzureAD/microsoft-authentication-library-for-python/pull/358/files#r629400328 + "login.microsoftonline.com", + "login.windows.net", + "sts.windows.net", + ) + else "{}.{}".format(region_to_use, central_authority.instance)) + return Authority( + "https://{}/{}".format(regional_host, central_authority.tenant), + self.http_client, + validate_authority=False) # The central_authority has already been validated + return None def _build_client(self, client_credential, authority): client_assertion = None @@ -289,15 +415,15 @@ client_assertion_type = Client.CLIENT_ASSERTION_TYPE_JWT else: default_body['client_secret'] = client_credential - server_configuration = { + central_configuration = { "authorization_endpoint": authority.authorization_endpoint, "token_endpoint": authority.token_endpoint, "device_authorization_endpoint": authority.device_authorization_endpoint or urljoin(authority.token_endpoint, "devicecode"), } - return Client( - server_configuration, + central_client = Client( + central_configuration, self.client_id, http_client=self.http_client, default_headers=default_headers, @@ -309,6 +435,31 @@ on_removing_rt=self.token_cache.remove_rt, on_updating_rt=self.token_cache.update_rt) + regional_client = None + if client_credential: # Currently regional endpoint only serves some CCA flows + regional_authority = self._get_regional_authority(authority) + if regional_authority: + regional_configuration = { + "authorization_endpoint": regional_authority.authorization_endpoint, + "token_endpoint": regional_authority.token_endpoint, + "device_authorization_endpoint": + regional_authority.device_authorization_endpoint or + urljoin(regional_authority.token_endpoint, "devicecode"), + } + regional_client = Client( + regional_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=lambda event: self.token_cache.add(dict( + event, environment=authority.instance)), + on_removing_rt=self.token_cache.remove_rt, + on_updating_rt=self.token_cache.update_rt) + return central_client, regional_client + def initiate_auth_code_flow( self, scopes, # type: list[str] @@ -325,7 +476,7 @@ you can use :func:`~acquire_token_by_auth_code_flow()` to complete the authentication/authorization. - :param list scope: + :param list scopes: It is a list of case-sensitive strings. :param str redirect_uri: Optional. If not specified, server will use the pre-registered one. @@ -373,7 +524,7 @@ flow = client.initiate_auth_code_flow( redirect_uri=redirect_uri, state=state, login_hint=login_hint, prompt=prompt, - scope=decorate_scope(scopes, self.client_id), + scope=self._decorate_scope(scopes), domain_hint=domain_hint, claims=_merge_claims_challenge_and_capabilities( self._client_capabilities, claims_challenge), @@ -455,7 +606,7 @@ response_type=response_type, redirect_uri=redirect_uri, state=state, login_hint=login_hint, prompt=prompt, - scope=decorate_scope(scopes, self.client_id), + scope=self._decorate_scope(scopes), nonce=nonce, domain_hint=domain_hint, claims=_merge_claims_challenge_and_capabilities( @@ -513,21 +664,21 @@ return redirect(url_for("index")) """ self._validate_ssh_cert_input_data(kwargs.get("data", {})) - return _clean_up(self.client.obtain_token_by_auth_code_flow( + telemetry_context = self._build_telemetry_context( + self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID) + response =_clean_up(self.client.obtain_token_by_auth_code_flow( auth_code_flow, auth_response, - scope=decorate_scope(scopes, self.client_id) if scopes else None, - headers={ - CLIENT_REQUEST_ID: _get_new_correlation_id(), - CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( - self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID), - }, + scope=self._decorate_scope(scopes) if scopes else None, + headers=telemetry_context.generate_headers(), data=dict( kwargs.pop("data", {}), claims=_merge_claims_challenge_and_capabilities( self._client_capabilities, auth_code_flow.pop("claims_challenge", None))), **kwargs)) + telemetry_context.update_telemetry(response) + return response def acquire_token_by_authorization_code( self, @@ -586,20 +737,20 @@ "Change your acquire_token_by_authorization_code() " "to acquire_token_by_auth_code_flow()", DeprecationWarning) with warnings.catch_warnings(record=True): - return _clean_up(self.client.obtain_token_by_authorization_code( + telemetry_context = self._build_telemetry_context( + self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID) + response = _clean_up(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), - }, + scope=self._decorate_scope(scopes), + headers=telemetry_context.generate_headers(), data=dict( kwargs.pop("data", {}), claims=_merge_claims_challenge_and_capabilities( self._client_capabilities, claims_challenge)), nonce=nonce, **kwargs)) + telemetry_context.update_telemetry(response) + return response def get_accounts(self, username=None): """Get a list of accounts which previously signed in, i.e. exists in cache. @@ -625,6 +776,13 @@ lowercase_username = username.lower() accounts = [a for a in accounts if a["username"].lower() == lowercase_username] + if not accounts: + logger.warning(( + "get_accounts(username='{}') finds no account. " + "If tokens were acquired without 'profile' scope, " + "they would contain no username for filtering. " + "Consider calling get_accounts(username=None) instead." + ).format(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, @@ -633,10 +791,25 @@ return accounts def _find_msal_accounts(self, environment): - return [a for a in self.token_cache.find( - TokenCache.CredentialType.ACCOUNT, query={"environment": environment}) + grouped_accounts = { + a.get("home_account_id"): # Grouped by home tenant's id + { # These are minimal amount of non-tenant-specific account info + "home_account_id": a.get("home_account_id"), + "environment": a.get("environment"), + "username": a.get("username"), + + # The following fields for backward compatibility, for now + "authority_type": a.get("authority_type"), + "local_account_id": a.get("local_account_id"), # Tenant-specific + "realm": a.get("realm"), # Tenant-specific + } + for a in self.token_cache.find( + TokenCache.CredentialType.ACCOUNT, + query={"environment": environment}) if a["authority_type"] in ( - TokenCache.AuthorityType.ADFS, TokenCache.AuthorityType.MSSTS)] + TokenCache.AuthorityType.ADFS, TokenCache.AuthorityType.MSSTS) + } + return list(grouped_accounts.values()) def _get_authority_aliases(self, instance): if not self.authority_groups: @@ -728,7 +901,7 @@ - None when cache lookup does not yield a token. """ result = self.acquire_token_silent_with_error( - scopes, account, authority, force_refresh, + scopes, account, authority=authority, force_refresh=force_refresh, claims_challenge=claims_challenge, **kwargs) return result if result and "error" not in result else None @@ -773,7 +946,7 @@ """ assert isinstance(scopes, list), "Invalid parameter type" self._validate_ssh_cert_input_data(kwargs.get("data", {})) - correlation_id = _get_new_correlation_id() + correlation_id = msal.telemetry._get_new_correlation_id() if authority: warnings.warn("We haven't decided how/if this method will accept authority parameter") # the_authority = Authority( @@ -844,9 +1017,11 @@ target=scopes, query=query) now = time.time() + refresh_reason = msal.telemetry.AT_ABSENT for entry in matches: expires_in = int(entry["expires_on"]) - now if expires_in < 5*60: # Then consider it expired + refresh_reason = msal.telemetry.AT_EXPIRED continue # Removal is not necessary, it will be overwritten logger.debug("Cache hit an AT") access_token_from_cache = { # Mimic a real response @@ -855,13 +1030,18 @@ "expires_in": int(expires_in), # OAuth2 specs defines it as int } if "refresh_on" in entry and int(entry["refresh_on"]) < now: # aging + refresh_reason = msal.telemetry.AT_AGING break # With a fallback in hand, we break here to go refresh + self._build_telemetry_context(-1).hit_an_access_token() return access_token_from_cache # It is still good as new + else: + refresh_reason = msal.telemetry.FORCE_REFRESH # TODO: It could also mean claims_challenge + assert refresh_reason, "It should have been established at this point" try: - result = 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, claims_challenge=claims_challenge, **kwargs) - result = _clean_up(result) + result = _clean_up(self._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family( + authority, self._decorate_scope(scopes), account, + refresh_reason=refresh_reason, claims_challenge=claims_challenge, + **kwargs)) if (result and "error" not in result) or (not access_token_from_cache): return result except: # The exact HTTP exception is transportation-layer dependent @@ -915,15 +1095,19 @@ 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, claims_challenge=None, **kwargs): + refresh_reason=None, correlation_id=None, claims_challenge=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) + client, _ = self._build_client(self.client_credential, authority) response = None # A distinguishable value to mean cache is empty + telemetry_context = self._build_telemetry_context( + self.ACQUIRE_TOKEN_SILENT_ID, + correlation_id=correlation_id, refresh_reason=refresh_reason) for entry in sorted( # Since unfit RTs would not be aggressively removed, # we start from newer RTs which are more likely fit. matches, @@ -941,16 +1125,13 @@ skip_account_creation=True, # To honor a concurrent remove_account() )), 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), - }, + headers=telemetry_context.generate_headers(), data=dict( kwargs.pop("data", {}), claims=_merge_claims_challenge_and_capabilities( self._client_capabilities, claims_challenge)), **kwargs) + telemetry_context.update_telemetry(response) if "error" not in response: return response logger.debug("Refresh failed. {error}: {error_description}".format( @@ -999,18 +1180,110 @@ * A dict contains no "error" key means migration was successful. """ self._validate_ssh_cert_input_data(kwargs.get("data", {})) - return _clean_up(self.client.obtain_token_by_refresh_token( + telemetry_context = self._build_telemetry_context( + self.ACQUIRE_TOKEN_BY_REFRESH_TOKEN, + refresh_reason=msal.telemetry.FORCE_REFRESH) + response = _clean_up(self.client.obtain_token_by_refresh_token( refresh_token, - 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_REFRESH_TOKEN), - }, + scope=self._decorate_scope(scopes), + headers=telemetry_context.generate_headers(), rt_getter=lambda rt: rt, on_updating_rt=False, on_removing_rt=lambda rt_item: None, # No OP **kwargs)) + telemetry_context.update_telemetry(response) + return response + + def acquire_token_by_username_password( + self, username, password, scopes, claims_challenge=None, **kwargs): + """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 + + :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). + :param claims_challenge: + The claims_challenge parameter requests specific claims requested by the resource provider + in the form of a claims_challenge directive in the www-authenticate header to be + returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token. + It is a string of a JSON object which contains lists of claims being requested from these locations. + + :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 = self._decorate_scope(scopes) + telemetry_context = self._build_telemetry_context( + self.ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID) + headers = telemetry_context.generate_headers() + data = dict( + kwargs.pop("data", {}), + claims=_merge_claims_challenge_and_capabilities( + self._client_capabilities, claims_challenge)) + if not self.authority.is_adfs: + user_realm_result = self.authority.user_realm_discovery( + username, correlation_id=headers[msal.telemetry.CLIENT_REQUEST_ID]) + if user_realm_result.get("account_type") == "Federated": + response = _clean_up(self._acquire_token_by_username_password_federated( + user_realm_result, username, password, scopes=scopes, + data=data, + headers=headers, **kwargs)) + telemetry_context.update_telemetry(response) + return response + response = _clean_up(self.client.obtain_token_by_username_password( + username, password, scope=scopes, + headers=headers, + data=data, + **kwargs)) + telemetry_context.update_telemetry(response) + return response + + def _acquire_token_by_username_password_federated( + self, user_realm_result, username, password, scopes=None, **kwargs): + wstrust_endpoint = {} + if user_realm_result.get("federation_metadata_url"): + wstrust_endpoint = mex_send_request( + user_realm_result["federation_metadata_url"], + self.http_client) + 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", "urn:federation:MicrosoftOnline"), + wstrust_endpoint.get("address", + # Fallback to an AAD supplied endpoint + user_realm_result.get("federation_active_auth_url")), + 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' + 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, + on_obtaining_tokens=lambda event: self.token_cache.add(dict( + event, + environment=self.authority.instance, + username=username, # Useful in case IDT contains no such info + )), + **kwargs) class PublicClientApplication(ClientApplication): # browser app or mobile app @@ -1039,7 +1312,7 @@ Prerequisite: In Azure Portal, configure the Redirect URI of your "Mobile and Desktop application" as ``http://localhost``. - :param list scope: + :param list scopes: It is a list of case-sensitive strings. :param str prompt: By default, no prompt value will be sent, not even "none". @@ -1080,15 +1353,16 @@ :return: - A dict containing no "error" key, - and typically contains an "access_token" key, - if cache lookup succeeded. + and typically contains an "access_token" key. - A dict containing an "error" key, when token refresh failed. """ self._validate_ssh_cert_input_data(kwargs.get("data", {})) claims = _merge_claims_challenge_and_capabilities( self._client_capabilities, claims_challenge) - return _clean_up(self.client.obtain_token_by_browser( - scope=decorate_scope(scopes, self.client_id) if scopes else None, + telemetry_context = self._build_telemetry_context( + self.ACQUIRE_TOKEN_INTERACTIVE) + response = _clean_up(self.client.obtain_token_by_browser( + scope=self._decorate_scope(scopes) if scopes else None, extra_scope_to_consent=extra_scopes_to_consent, redirect_uri="http://localhost:{port}".format( # Hardcode the host, for now. AAD portal rejects 127.0.0.1 anyway @@ -1101,12 +1375,10 @@ "domain_hint": domain_hint, }, data=dict(kwargs.pop("data", {}), claims=claims), - headers={ - CLIENT_REQUEST_ID: _get_new_correlation_id(), - CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( - self.ACQUIRE_TOKEN_INTERACTIVE), - }, + headers=telemetry_context.generate_headers(), **kwargs)) + telemetry_context.update_telemetry(response) + return response def initiate_device_flow(self, scopes=None, **kwargs): """Initiate a Device Flow instance, @@ -1119,13 +1391,10 @@ - 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() + correlation_id = msal.telemetry._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 - }, + scope=self._decorate_scope(scopes or []), + headers={msal.telemetry.CLIENT_REQUEST_ID: correlation_id}, **kwargs) flow[self.DEVICE_FLOW_CORRELATION_ID] = correlation_id return flow @@ -1149,7 +1418,10 @@ - A successful response would contain "access_token" key, - an error response would contain "error" and usually "error_description". """ - return _clean_up(self.client.obtain_token_by_device_flow( + telemetry_context = self._build_telemetry_context( + self.ACQUIRE_TOKEN_BY_DEVICE_FLOW_ID, + correlation_id=flow.get(self.DEVICE_FLOW_CORRELATION_ID)) + response = _clean_up(self.client.obtain_token_by_device_flow( flow, data=dict( kwargs.pop("data", {}), @@ -1159,96 +1431,10 @@ claims=_merge_claims_challenge_and_capabilities( self._client_capabilities, claims_challenge), ), - 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), - }, + headers=telemetry_context.generate_headers(), **kwargs)) - - def acquire_token_by_username_password( - self, username, password, scopes, claims_challenge=None, **kwargs): - """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 - - :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). - :param claims_challenge: - The claims_challenge parameter requests specific claims requested by the resource provider - in the form of a claims_challenge directive in the www-authenticate header to be - returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token. - It is a string of a JSON object which contains lists of claims being requested from these locations. - - :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), - } - data = dict( - kwargs.pop("data", {}), - claims=_merge_claims_challenge_and_capabilities( - self._client_capabilities, claims_challenge)) - 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 _clean_up(self._acquire_token_by_username_password_federated( - user_realm_result, username, password, scopes=scopes, - data=data, - headers=headers, **kwargs)) - return _clean_up(self.client.obtain_token_by_username_password( - username, password, scope=scopes, - headers=headers, - data=data, - **kwargs)) - - def _acquire_token_by_username_password_federated( - self, user_realm_result, username, password, scopes=None, **kwargs): - wstrust_endpoint = {} - if user_realm_result.get("federation_metadata_url"): - wstrust_endpoint = mex_send_request( - user_realm_result["federation_metadata_url"], - self.http_client) - 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", "urn:federation:MicrosoftOnline"), - wstrust_endpoint.get("address", - # Fallback to an AAD supplied endpoint - user_realm_result.get("federation_active_auth_url")), - 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' - 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) + telemetry_context.update_telemetry(response) + return response class ConfidentialClientApplication(ClientApplication): # server-side web app @@ -1271,18 +1457,19 @@ """ # TBD: force_refresh behavior self._validate_ssh_cert_input_data(kwargs.get("data", {})) - return _clean_up(self.client.obtain_token_for_client( + telemetry_context = self._build_telemetry_context( + self.ACQUIRE_TOKEN_FOR_CLIENT_ID) + client = self._regional_client or self.client + response = _clean_up(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), - }, + headers=telemetry_context.generate_headers(), data=dict( kwargs.pop("data", {}), claims=_merge_claims_challenge_and_capabilities( self._client_capabilities, claims_challenge)), **kwargs)) + telemetry_context.update_telemetry(response) + return response def acquire_token_on_behalf_of(self, user_assertion, scopes, claims_challenge=None, **kwargs): """Acquires token using on-behalf-of (OBO) flow. @@ -1310,12 +1497,14 @@ - A successful response would contain "access_token" key, - an error response would contain "error" and usually "error_description". """ + telemetry_context = self._build_telemetry_context( + self.ACQUIRE_TOKEN_ON_BEHALF_OF_ID) # The implementation is NOT based on Token Exchange # https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16 - return _clean_up(self.client.obtain_token_by_assertion( # bases on assertion RFC 7521 + response = _clean_up(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: + scope=self._decorate_scope(scopes), # 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) @@ -1326,9 +1515,7 @@ requested_token_use="on_behalf_of", claims=_merge_claims_challenge_and_capabilities( self._client_capabilities, claims_challenge)), - headers={ - CLIENT_REQUEST_ID: _get_new_correlation_id(), - CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( - self.ACQUIRE_TOKEN_ON_BEHALF_OF_ID), - }, + headers=telemetry_context.generate_headers(), **kwargs)) + telemetry_context.update_telemetry(response) + return response diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/msal/oauth2cli/authcode.py new/msal-1.12.0/msal/oauth2cli/authcode.py --- old/msal-1.10.0/msal/oauth2cli/authcode.py 2021-03-08 21:46:19.000000000 +0100 +++ new/msal-1.12.0/msal/oauth2cli/authcode.py 2021-05-19 22:28:05.000000000 +0200 @@ -33,9 +33,34 @@ ).get("code") +def is_wsl(): + # "Official" way of detecting WSL: https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364 + # Run `uname -a` to get 'release' without python + # - WSL 1: '4.4.0-19041-Microsoft' + # - WSL 2: '4.19.128-microsoft-standard' + import platform + uname = platform.uname() + platform_name = getattr(uname, 'system', uname[0]).lower() + release = getattr(uname, 'release', uname[2]).lower() + return platform_name == 'linux' and 'microsoft' in release + + def _browse(auth_uri): # throws ImportError, possibly webbrowser.Error in future import webbrowser # Lazy import. Some distro may not have this. - return webbrowser.open(auth_uri) # Use default browser. Customizable by $BROWSER + browser_opened = webbrowser.open(auth_uri) # Use default browser. Customizable by $BROWSER + + # In WSL which doesn't have www-browser, try launching browser with PowerShell + if not browser_opened and is_wsl(): + try: + import subprocess + # https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe + # Ampersand (&) should be quoted + exit_code = subprocess.call( + ['powershell.exe', '-NoProfile', '-Command', 'Start-Process "{}"'.format(auth_uri)]) + browser_opened = exit_code == 0 + except FileNotFoundError: # WSL might be too old + pass + return browser_opened def _qs2kv(qs): @@ -245,4 +270,3 @@ timeout=60, state=flow["state"], # Optional ), indent=4)) - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/msal/oauth2cli/oauth2.py new/msal-1.12.0/msal/oauth2cli/oauth2.py --- old/msal-1.10.0/msal/oauth2cli/oauth2.py 2021-03-08 21:46:19.000000000 +0100 +++ new/msal-1.12.0/msal/oauth2cli/oauth2.py 2021-05-19 22:28:05.000000000 +0200 @@ -770,7 +770,6 @@ rt_getter=lambda token_item: token_item["refresh_token"], on_removing_rt=None, on_updating_rt=None, - on_obtaining_tokens=None, **kwargs): # type: (Union[str, dict], Union[str, list, set, tuple], Callable) -> dict """This is an overload which will trigger token storage callbacks. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/msal/oauth2cli/oidc.py new/msal-1.12.0/msal/oauth2cli/oidc.py --- old/msal-1.10.0/msal/oauth2cli/oidc.py 2021-03-08 21:46:19.000000000 +0100 +++ new/msal-1.12.0/msal/oauth2cli/oidc.py 2021-05-19 22:28:05.000000000 +0200 @@ -83,6 +83,19 @@ return hashlib.sha256(nonce.encode("ascii")).hexdigest() +class Prompt(object): + """This class defines the constant strings for prompt parameter. + + The values are based on + https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + """ + NONE = "none" + LOGIN = "login" + CONSENT = "consent" + SELECT_ACCOUNT = "select_account" + CREATE = "create" # Defined in https://openid.net/specs/openid-connect-prompt-create-1_0.html#PromptParameter + + class Client(oauth2.Client): """OpenID Connect is a layer on top of the OAuth2. @@ -217,6 +230,8 @@ `OIDC <https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest>`_. :param string prompt: Defined in `OIDC <https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest>`_. + You can find the valid string values defined in :class:`oidc.Prompt`. + :param int max_age: Defined in `OIDC <https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest>`_. :param string ui_locales: Defined in @@ -232,7 +247,7 @@ for descriptions on other parameters and return value. """ filtered_params = {k:v for k, v in dict( - prompt=prompt, + prompt=" ".join(prompt) if isinstance(prompt, (list, tuple)) else prompt, display=display, max_age=max_age, ui_locales=ui_locales, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/msal/region.py new/msal-1.12.0/msal/region.py --- old/msal-1.10.0/msal/region.py 1970-01-01 01:00:00.000000000 +0100 +++ new/msal-1.12.0/msal/region.py 2021-05-19 22:28:05.000000000 +0200 @@ -0,0 +1,47 @@ +import os +import logging + +logger = logging.getLogger(__name__) + + +def _detect_region(http_client=None): + region = _detect_region_of_azure_function() # It is cheap, so we do it always + if http_client and not region: + return _detect_region_of_azure_vm(http_client) # It could hang for minutes + return region + + +def _detect_region_of_azure_function(): + return os.environ.get("REGION_NAME") + + +def _detect_region_of_azure_vm(http_client): + url = ( + "http://169.254.169.254/metadata/instance" + + # Utilize the "route parameters" feature to obtain region as a string + # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#route-parameters + "/compute/location?format=text" + + # Location info is available since API version 2017-04-02 + # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#response-1 + "&api-version=2021-01-01" + ) + logger.info( + "Connecting to IMDS {}. " + "It may take a while if you are running outside of Azure. " + "You should consider opting in/out region behavior on-demand, " + 'by loading a boolean flag "is_deployed_in_azure" ' + 'from your per-deployment config and then do ' + '"app = ConfidentialClientApplication(..., ' + 'azure_region=is_deployed_in_azure)"'.format(url)) + try: + # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#instance-metadata + resp = http_client.get(url, headers={"Metadata": "true"}) + except: + logger.info( + "IMDS {} unavailable. Perhaps not running in Azure VM?".format(url)) + return None + else: + return resp.text.strip() + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/msal/telemetry.py new/msal-1.12.0/msal/telemetry.py --- old/msal-1.10.0/msal/telemetry.py 1970-01-01 01:00:00.000000000 +0100 +++ new/msal-1.12.0/msal/telemetry.py 2021-05-19 22:28:05.000000000 +0200 @@ -0,0 +1,78 @@ +import uuid +import logging + + +logger = logging.getLogger(__name__) + +CLIENT_REQUEST_ID = 'client-request-id' +CLIENT_CURRENT_TELEMETRY = "x-client-current-telemetry" +CLIENT_LAST_TELEMETRY = "x-client-last-telemetry" +NON_SILENT_CALL = 0 +FORCE_REFRESH = 1 +AT_ABSENT = 2 +AT_EXPIRED = 3 +AT_AGING = 4 +RESERVED = 5 + + +def _get_new_correlation_id(): + return str(uuid.uuid4()) + + +class _TelemetryContext(object): + """It is used for handling the telemetry context for current OAuth2 "exchange".""" + # https://identitydivision.visualstudio.com/DevEx/_git/AuthLibrariesApiReview?path=%2FTelemetry%2FMSALServerSideTelemetry.md&_a=preview + _SUCCEEDED = "succeeded" + _FAILED = "failed" + _FAILURE_SIZE = "failure_size" + _CURRENT_HEADER_SIZE_LIMIT = 100 + _LAST_HEADER_SIZE_LIMIT = 350 + + def __init__(self, buffer, lock, api_id, correlation_id=None, refresh_reason=None): + self._buffer = buffer + self._lock = lock + self._api_id = api_id + self._correlation_id = correlation_id or _get_new_correlation_id() + self._refresh_reason = refresh_reason or NON_SILENT_CALL + logger.debug("Generate or reuse correlation_id: %s", self._correlation_id) + + def generate_headers(self): + with self._lock: + current = "4|{api_id},{cache_refresh}|".format( + api_id=self._api_id, cache_refresh=self._refresh_reason) + if len(current) > self._CURRENT_HEADER_SIZE_LIMIT: + logger.warning( + "Telemetry header greater than {} will be truncated by AAD".format( + self._CURRENT_HEADER_SIZE_LIMIT)) + failures = self._buffer.get(self._FAILED, []) + return { + CLIENT_REQUEST_ID: self._correlation_id, + CLIENT_CURRENT_TELEMETRY: current, + CLIENT_LAST_TELEMETRY: "4|{succeeded}|{failed_requests}|{errors}|".format( + succeeded=self._buffer.get(self._SUCCEEDED, 0), + failed_requests=",".join("{a},{c}".format(**f) for f in failures), + errors=",".join(f["e"] for f in failures), + ) + } + + def hit_an_access_token(self): + with self._lock: + self._buffer[self._SUCCEEDED] = self._buffer.get(self._SUCCEEDED, 0) + 1 + + def update_telemetry(self, auth_result): + if auth_result: + with self._lock: + if "error" in auth_result: + self._record_failure(auth_result["error"]) + else: # Telemetry sent successfully. Reset buffer + self._buffer.clear() # This won't work: self._buffer = {} + + def _record_failure(self, error): + simulation = len(",{api_id},{correlation_id},{error}".format( + api_id=self._api_id, correlation_id=self._correlation_id, error=error)) + if self._buffer.get(self._FAILURE_SIZE, 0) + simulation < self._LAST_HEADER_SIZE_LIMIT: + self._buffer[self._FAILURE_SIZE] = self._buffer.get( + self._FAILURE_SIZE, 0) + simulation + self._buffer.setdefault(self._FAILED, []).append({ + "a": self._api_id, "c": self._correlation_id, "e": error}) + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/msal/token_cache.py new/msal-1.12.0/msal/token_cache.py --- old/msal-1.10.0/msal/token_cache.py 2021-03-08 21:46:19.000000000 +0100 +++ new/msal-1.12.0/msal/token_cache.py 2021-05-19 22:28:05.000000000 +0200 @@ -108,11 +108,13 @@ if sensitive in dictionary: dictionary[sensitive] = "********" wipe(event.get("data", {}), - ("password", "client_secret", "refresh_token", "assertion", "username")) + ("password", "client_secret", "refresh_token", "assertion")) try: return self.__add(event, now=now) finally: - wipe(event.get("response", {}), ("access_token", "refresh_token")) + wipe(event.get("response", {}), ( # These claims were useful during __add() + "access_token", "refresh_token", "id_token", "username")) + wipe(event, ["username"]) # Needed for federated ROPC 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. @@ -145,7 +147,7 @@ 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 + target = ' '.join(event.get("scope") or []) # Per schema, we don't sort it with self._lock: now = int(time.time() if now is None else now) @@ -184,6 +186,8 @@ "oid", id_token_claims.get("sub")), "username": id_token_claims.get("preferred_username") # AAD or id_token_claims.get("upn") # ADFS 2019 + or data.get("username") # Falls back to ROPC username + or event.get("username") # Falls back to Federated ROPC username or "", # The schema does not like null "authority_type": self.AuthorityType.ADFS if realm == "adfs" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/msal/wstrust_response.py new/msal-1.12.0/msal/wstrust_response.py --- old/msal-1.10.0/msal/wstrust_response.py 2021-03-08 21:46:19.000000000 +0100 +++ new/msal-1.12.0/msal/wstrust_response.py 2021-05-19 22:28:05.000000000 +0200 @@ -88,5 +88,7 @@ token_types = findall_content(rstr, "TokenType") tokens = findall_content(rstr, "RequestedSecurityToken") if token_types and tokens: - return {"token": tokens[0].encode('us-ascii'), "type": token_types[0]} + # Historically, we use "us-ascii" encoding, but it should be "utf-8" + # https://stackoverflow.com/questions/36658000/what-is-encoding-used-for-saml-conversations + return {"token": tokens[0].encode('utf-8'), "type": token_types[0]} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/msal.egg-info/PKG-INFO new/msal-1.12.0/msal.egg-info/PKG-INFO --- old/msal-1.10.0/msal.egg-info/PKG-INFO 2021-03-08 21:46:32.000000000 +0100 +++ new/msal-1.12.0/msal.egg-info/PKG-INFO 2021-05-19 22:28:13.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: msal -Version: 1.10.0 +Version: 1.12.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 @@ -21,8 +21,14 @@ 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) - | --- | --- | --- | --- | + | [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) | [Feedback](https://forms.office.com/r/TMjZkDbzjY) | + | --- | --- | --- | --- | --- | + + ## Scenarios supported + + Click on the following thumbnail to visit a large map with clickable links to proper samples. + + [](https://msal-python.readthedocs.io/en/latest/) ## Installation @@ -135,6 +141,9 @@ Here is the latest Q&A on Stack Overflow for MSAL: [http://stackoverflow.com/questions/tagged/msal](http://stackoverflow.com/questions/tagged/msal) + ## Submit Feedback + We'd like your thoughts on this library. Please complete [this short survey.](https://forms.office.com/r/TMjZkDbzjY) + ## Security Reporting If you find a security issue with our libraries or services please report it to [sec...@microsoft.com](mailto:sec...@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. @@ -153,11 +162,11 @@ Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.3 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Description-Content-Type: text/markdown diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/msal.egg-info/SOURCES.txt new/msal-1.12.0/msal.egg-info/SOURCES.txt --- old/msal-1.10.0/msal.egg-info/SOURCES.txt 2021-03-08 21:46:32.000000000 +0100 +++ new/msal-1.12.0/msal.egg-info/SOURCES.txt 2021-05-19 22:28:13.000000000 +0200 @@ -1,3 +1,4 @@ +LICENSE README.md setup.cfg setup.py @@ -6,6 +7,8 @@ msal/authority.py msal/exceptions.py msal/mex.py +msal/region.py +msal/telemetry.py msal/token_cache.py msal/wstrust_request.py msal/wstrust_response.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/msal.egg-info/requires.txt new/msal-1.12.0/msal.egg-info/requires.txt --- old/msal-1.10.0/msal.egg-info/requires.txt 2021-03-08 21:46:32.000000000 +0100 +++ new/msal-1.12.0/msal.egg-info/requires.txt 2021-05-19 22:28:13.000000000 +0200 @@ -1,3 +1,6 @@ requests<3,>=2.0.0 PyJWT[crypto]<3,>=1.0.0 cryptography<4,>=0.6 + +[:python_version < "3.3"] +mock diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.10.0/setup.py new/msal-1.12.0/setup.py --- old/msal-1.10.0/setup.py 2021-03-08 21:46:19.000000000 +0100 +++ new/msal-1.12.0/setup.py 2021-05-19 22:28:06.000000000 +0200 @@ -58,11 +58,11 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', ], @@ -84,6 +84,7 @@ # We will go with "<4" for now, which is also what our another dependency, # pyjwt, currently use. + "mock;python_version<'3.3'", ] )