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 2024-01-04 15:59:37 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-msal (Old) and /work/SRC/openSUSE:Factory/.python-msal.new.28375 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-msal" Thu Jan 4 15:59:37 2024 rev:18 rq:1136736 version:1.26.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-msal/python-msal.changes 2023-11-26 19:38:49.877524144 +0100 +++ /work/SRC/openSUSE:Factory/.python-msal.new.28375/python-msal.changes 2024-01-04 16:01:28.845468751 +0100 @@ -1,0 +2,7 @@ +Fri Dec 8 13:02:43 UTC 2023 - John Paul Adrian Glaubitz <[email protected]> + +- Update to version 1.26.0 + * Do not auto-detect region if app developer does not opt-in to region (#629, #630) + * Support Proof-of-Possession (PoP) for Public Client based on broker (#511) + +------------------------------------------------------------------- Old: ---- msal-1.25.0.tar.gz New: ---- msal-1.26.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-msal.spec ++++++ --- /var/tmp/diff_new_pack.Ced7hm/_old 2024-01-04 16:01:29.313485848 +0100 +++ /var/tmp/diff_new_pack.Ced7hm/_new 2024-01-04 16:01:29.313485848 +0100 @@ -21,7 +21,7 @@ %define skip_python2 1 %endif Name: python-msal -Version: 1.25.0 +Version: 1.26.0 Release: 0 Summary: Microsoft Authentication Library (MSAL) for Python License: MIT ++++++ msal-1.25.0.tar.gz -> msal-1.26.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.25.0/PKG-INFO new/msal-1.26.0/PKG-INFO --- old/msal-1.25.0/PKG-INFO 2023-11-04 01:14:35.341285200 +0100 +++ new/msal-1.26.0/PKG-INFO 2023-12-05 10:08:03.082436000 +0100 @@ -1,7 +1,7 @@ Metadata-Version: 2.1 Name: msal -Version: 1.25.0 -Summary: The Microsoft Authentication Library (MSAL) for Python library +Version: 1.26.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 Author-email: [email protected] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.25.0/msal/__init__.py new/msal-1.26.0/msal/__init__.py --- old/msal-1.25.0/msal/__init__.py 2023-11-04 01:14:20.000000000 +0100 +++ new/msal-1.26.0/msal/__init__.py 2023-12-05 10:07:48.000000000 +0100 @@ -33,4 +33,5 @@ ) from .oauth2cli.oidc import Prompt from .token_cache import TokenCache, SerializableTokenCache +from .auth_scheme import PopAuthScheme diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.25.0/msal/__main__.py new/msal-1.26.0/msal/__main__.py --- old/msal-1.25.0/msal/__main__.py 2023-11-04 01:14:20.000000000 +0100 +++ new/msal-1.26.0/msal/__main__.py 2023-12-05 10:07:48.000000000 +0100 @@ -22,6 +22,11 @@ _AZURE_CLI = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" _VISUAL_STUDIO = "04f0c124-f2bc-4f59-8241-bf6df9866bbd" +placeholder_auth_scheme = msal.PopAuthScheme( + http_method=msal.PopAuthScheme.HTTP_GET, + url="https://example.com/endpoint", + nonce="placeholder", + ) def print_json(blob): print(json.dumps(blob, indent=2, sort_keys=True)) @@ -88,6 +93,9 @@ _input_scopes(), account=account, force_refresh=_input_boolean("Bypass MSAL Python's token cache?"), + auth_scheme=placeholder_auth_scheme + if app.is_pop_supported() and _input_boolean("Acquire AT POP via Broker?") + else None, )) def _acquire_token_interactive(app, scopes=None, data=None): @@ -117,6 +125,9 @@ ], # Here this test app mimics the setting for some known MSA-PT apps port=1234, # Hard coded for testing. Real app typically uses default value. prompt=prompt, login_hint=login_hint, data=data or {}, + auth_scheme=placeholder_auth_scheme + if app.is_pop_supported() and _input_boolean("Acquire AT POP via Broker?") + else None, ) if login_hint and "id_token_claims" in result: signed_in_user = result.get("id_token_claims", {}).get("preferred_username") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.25.0/msal/application.py new/msal-1.26.0/msal/application.py --- old/msal-1.25.0/msal/application.py 2023-11-04 01:14:20.000000000 +0100 +++ new/msal-1.26.0/msal/application.py 2023-12-05 10:07:48.000000000 +0100 @@ -25,7 +25,7 @@ # The __init__.py will import this. Not the other way around. -__version__ = "1.25.0" # When releasing, also check and bump our dependencies's versions if needed +__version__ = "1.26.0" # When releasing, also check and bump our dependencies's versions if needed logger = logging.getLogger(__name__) _AUTHORITY_TYPE_CLOUDSHELL = "CLOUDSHELL" @@ -182,6 +182,10 @@ _TOKEN_SOURCE_BROKER = "broker" _enable_broker = False + _AUTH_SCHEME_UNSUPPORTED = ( + "auth_scheme is currently only available from broker. " + "You can enable broker by following these instructions. " + "https://msal-python.readthedocs.io/en/latest/#publicclientapplication") def __init__( self, client_id, @@ -336,51 +340,22 @@ `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: - AAD provides regional endpoints for apps to opt in - to keep their traffic remain inside that region. - - 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. - - This parameter defaults to None, which means region behavior remains off. - - 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. + :param str azure_region: (optional) + Instructs MSAL to use the Entra regional token service. This legacy feature is only available to + first-party applications. Only ``acquire_token_for_client()`` is supported. + + Supports 3 values: + + ``azure_region=None`` - meaning no region is used. This is the default value. + ``azure_region="some_region"`` - meaning the specified region is used. + ``azure_region=True`` - meaning MSAL will try to auto-detect the region. This is not recommended. .. note:: + Region auto-discovery has been tested on VMs and on Azure Functions. It is unreliable. + Applications using this option should configure a short timeout. - 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. + For more details and for the values of the region string + see https://learn.microsoft.com/entra/msal/dotnet/resources/region-discovery-troubleshooting New in version 1.12.0. @@ -586,6 +561,10 @@ "We will fallback to non-broker.") logger.debug("Broker enabled? %s", self._enable_broker) + def is_pop_supported(self): + """Returns True if this client supports Proof-of-Possession Access Token.""" + return self._enable_broker + def _decorate_scope( self, scopes, reserved_scope=frozenset(['openid', 'profile', 'offline_access'])): @@ -612,6 +591,8 @@ correlation_id=correlation_id, refresh_reason=refresh_reason) def _get_regional_authority(self, central_authority): + if not self._region_configured: # User did not opt-in to ESTS-R + return None # Short circuit to completely bypass region detection self._region_detected = self._region_detected or _detect_region( self.http_client if self._region_configured is not None else None) if (self._region_configured != self.ATTEMPT_REGION_DISCOVERY @@ -1212,6 +1193,7 @@ authority=None, # See get_authorization_request_url() force_refresh=False, # type: Optional[boolean] claims_challenge=None, + auth_scheme=None, **kwargs): """Acquire an access token for given account, without user interaction. @@ -1232,7 +1214,7 @@ return None # A backward-compatible NO-OP to drop the account=None usage result = _clean_up(self._acquire_token_silent_with_error( scopes, account, authority=authority, force_refresh=force_refresh, - claims_challenge=claims_challenge, **kwargs)) + claims_challenge=claims_challenge, auth_scheme=auth_scheme, **kwargs)) return result if result and "error" not in result else None def acquire_token_silent_with_error( @@ -1242,6 +1224,7 @@ authority=None, # See get_authorization_request_url() force_refresh=False, # type: Optional[boolean] claims_challenge=None, + auth_scheme=None, **kwargs): """Acquire an access token for given account, without user interaction. @@ -1268,6 +1251,12 @@ 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. + :param object auth_scheme: + You can provide an ``msal.auth_scheme.PopAuthScheme`` object + so that MSAL will get a Proof-of-Possession (POP) token for you. + + New in version 1.26.0. + :return: - A dict containing no "error" key, and typically contains an "access_token" key, @@ -1279,7 +1268,7 @@ return None # A backward-compatible NO-OP to drop the account=None usage return _clean_up(self._acquire_token_silent_with_error( scopes, account, authority=authority, force_refresh=force_refresh, - claims_challenge=claims_challenge, **kwargs)) + claims_challenge=claims_challenge, auth_scheme=auth_scheme, **kwargs)) def _acquire_token_silent_with_error( self, @@ -1288,6 +1277,7 @@ authority=None, # See get_authorization_request_url() force_refresh=False, # type: Optional[boolean] claims_challenge=None, + auth_scheme=None, **kwargs): assert isinstance(scopes, list), "Invalid parameter type" self._validate_ssh_cert_input_data(kwargs.get("data", {})) @@ -1303,6 +1293,7 @@ scopes, account, self.authority, force_refresh=force_refresh, claims_challenge=claims_challenge, correlation_id=correlation_id, + auth_scheme=auth_scheme, **kwargs) if result and "error" not in result: return result @@ -1325,6 +1316,7 @@ scopes, account, the_authority, force_refresh=force_refresh, claims_challenge=claims_challenge, correlation_id=correlation_id, + auth_scheme=auth_scheme, **kwargs) if result: if "error" not in result: @@ -1349,12 +1341,13 @@ claims_challenge=None, correlation_id=None, http_exceptions=None, + auth_scheme=None, **kwargs): # This internal method has two calling patterns: # it accepts a non-empty account to find token for a user, # and accepts account=None to find a token for the current app. access_token_from_cache = None - if not (force_refresh or claims_challenge): # Bypass AT when desired or using claims + if not (force_refresh or claims_challenge or auth_scheme): # Then attempt AT cache query={ "client_id": self.client_id, "environment": authority.instance, @@ -1397,6 +1390,8 @@ try: data = kwargs.get("data", {}) if account and account.get("authority_type") == _AUTHORITY_TYPE_CLOUDSHELL: + if auth_scheme: + raise ValueError("auth_scheme is not supported in Cloud Shell") return self._acquire_token_by_cloud_shell(scopes, data=data) if self._enable_broker and account and account.get("account_source") in ( @@ -1412,6 +1407,7 @@ claims=_merge_claims_challenge_and_capabilities( self._client_capabilities, claims_challenge), correlation_id=correlation_id, + auth_scheme=auth_scheme, **data) if response: # Broker provides a decisive outcome account_was_established_by_broker = account.get( @@ -1420,6 +1416,8 @@ if account_was_established_by_broker or broker_attempt_succeeded_just_now: return self._process_broker_response(response, scopes, data) + if auth_scheme: + raise ValueError(self._AUTH_SCHEME_UNSUPPORTED) if account: result = self._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family( authority, self._decorate_scope(scopes), account, @@ -1615,7 +1613,11 @@ return response def acquire_token_by_username_password( - self, username, password, scopes, claims_challenge=None, **kwargs): + self, username, password, scopes, claims_challenge=None, + # Note: We shouldn't need to surface enable_msa_passthrough, + # because this ROPC won't work with MSA account anyway. + auth_scheme=None, + **kwargs): """Gets a token for a given resource via user credentials. See this page for constraints of Username Password Flow. @@ -1631,6 +1633,12 @@ 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. + :param object auth_scheme: + You can provide an ``msal.auth_scheme.PopAuthScheme`` object + so that MSAL will get a Proof-of-Possession (POP) token for you. + + New in version 1.26.0. + :return: A dict representing the json response from AAD: - A successful response would contain "access_token" key, @@ -1650,9 +1658,12 @@ self.authority._is_known_to_developer or self._instance_discovery is False) else None, claims=claims, + auth_scheme=auth_scheme, ) return self._process_broker_response(response, scopes, kwargs.get("data", {})) + if auth_scheme: + raise ValueError(self._AUTH_SCHEME_UNSUPPORTED) scopes = self._decorate_scope(scopes) telemetry_context = self._build_telemetry_context( self.ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID) @@ -1771,6 +1782,8 @@ :param boolean enable_broker_on_windows: This setting is only effective if your app is running on Windows 10+. This parameter defaults to None, which means MSAL will not utilize a broker. + + New in MSAL Python 1.25.0. """ if client_credential is not None: raise ValueError("Public Client should not possess credentials") @@ -1793,6 +1806,7 @@ max_age=None, parent_window_handle=None, on_before_launching_ui=None, + auth_scheme=None, **kwargs): """Acquire token interactively i.e. via a local browser. @@ -1868,6 +1882,12 @@ New in version 1.20.0. + :param object auth_scheme: + You can provide an ``msal.auth_scheme.PopAuthScheme`` object + so that MSAL will get a Proof-of-Possession (POP) token for you. + + New in version 1.26.0. + :return: - A dict containing no "error" key, and typically contains an "access_token" key. @@ -1912,12 +1932,15 @@ claims, data, on_before_launching_ui, + auth_scheme, prompt=prompt, login_hint=login_hint, max_age=max_age, ) return self._process_broker_response(response, scopes, data) + if auth_scheme: + raise ValueError("auth_scheme is currently only available from broker") on_before_launching_ui(ui="browser") telemetry_context = self._build_telemetry_context( self.ACQUIRE_TOKEN_INTERACTIVE) @@ -1952,6 +1975,7 @@ claims, # type: str data, # type: dict on_before_launching_ui, # type: callable + auth_scheme, # type: object prompt=None, login_hint=None, # type: Optional[str] max_age=None, @@ -1975,6 +1999,7 @@ accounts[0]["local_account_id"], scopes, claims=claims, + auth_scheme=auth_scheme, **data) if response and "error" not in response: return response @@ -1987,6 +2012,7 @@ claims=claims, max_age=max_age, enable_msa_pt=enable_msa_passthrough, + auth_scheme=auth_scheme, **data) is_wrong_account = bool( # _signin_silently() only gets tokens for default account, @@ -2027,6 +2053,7 @@ claims=claims, max_age=max_age, enable_msa_pt=enable_msa_passthrough, + auth_scheme=auth_scheme, **data) def initiate_device_flow(self, scopes=None, **kwargs): @@ -2174,8 +2201,7 @@ """ 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 + # The implementation is NOT based on Token Exchange (RFC 8693) 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.25.0/msal/auth_scheme.py new/msal-1.26.0/msal/auth_scheme.py --- old/msal-1.25.0/msal/auth_scheme.py 1970-01-01 01:00:00.000000000 +0100 +++ new/msal-1.26.0/msal/auth_scheme.py 2023-12-05 10:07:48.000000000 +0100 @@ -0,0 +1,34 @@ +try: + from urllib.parse import urlparse +except ImportError: # Fall back to Python 2 + from urlparse import urlparse + +# We may support more auth schemes in the future +class PopAuthScheme(object): + HTTP_GET = "GET" + HTTP_POST = "POST" + HTTP_PUT = "PUT" + HTTP_DELETE = "DELETE" + HTTP_PATCH = "PATCH" + _HTTP_METHODS = (HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_DELETE, HTTP_PATCH) + # Internal design: https://identitydivision.visualstudio.com/DevEx/_git/AuthLibrariesApiReview?path=/PoPTokensProtocol/PopTokensProtocol.md + def __init__(self, http_method=None, url=None, nonce=None): + """Create an auth scheme which is needed to obtain a Proof-of-Possession token. + + :param str http_method: + Its value is an uppercase http verb, such as "GET" and "POST". + :param str url: + The url to be signed. + :param str nonce: + The nonce came from resource's challenge. + """ + if not (http_method and url and nonce): + # In the future, we may also support accepting an http_response as input + raise ValueError("All http_method, url and nonce are required parameters") + if http_method not in self._HTTP_METHODS: + raise ValueError("http_method must be uppercase, according to " + "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3") + self._http_method = http_method + self._url = urlparse(url) + self._nonce = nonce + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.25.0/msal/broker.py new/msal-1.26.0/msal/broker.py --- old/msal-1.25.0/msal/broker.py 2023-11-04 01:14:20.000000000 +0100 +++ new/msal-1.26.0/msal/broker.py 2023-12-05 10:07:48.000000000 +0100 @@ -99,13 +99,17 @@ assert account, "Account is expected to be always available" # Note: There are more account attribute getters available in pymsalruntime 0.13+ return_value = {k: v for k, v in { - "access_token": result.get_access_token(), + "access_token": + result.get_authorization_header() # It returns "pop SignedHttpRequest" + .split()[1] + if result.is_pop_authorization() else result.get_access_token(), "expires_in": result.get_access_token_expiry_time() - int(time.time()), # Convert epoch to count-down "id_token": result.get_raw_id_token(), # New in pymsalruntime 0.8.1 "id_token_claims": id_token_claims, "client_info": account.get_client_info(), "_account_id": account.get_account_id(), - "token_type": expected_token_type or "Bearer", # Workaround its absence from broker + "token_type": "pop" if result.is_pop_authorization() else ( + expected_token_type or "bearer"), # Workaround "ssh-cert"'s absence from broker }.items() if v} likely_a_cert = return_value["access_token"].startswith("AAAA") # Empirical observation if return_value["token_type"].lower() == "ssh-cert" and not likely_a_cert: @@ -128,11 +132,16 @@ def _signin_silently( authority, client_id, scopes, correlation_id=None, claims=None, enable_msa_pt=False, + auth_scheme=None, **kwargs): params = pymsalruntime.MSALRuntimeAuthParameters(client_id, authority) params.set_requested_scopes(scopes) if claims: params.set_decoded_claims(claims) + if auth_scheme: + params.set_pop_params( + auth_scheme._http_method, auth_scheme._url.netloc, auth_scheme._url.path, + auth_scheme._nonce) callback_data = _CallbackData() for k, v in kwargs.items(): # This can be used to support domain_hint, max_age, etc. if v is not None: @@ -156,6 +165,7 @@ claims=None, correlation_id=None, enable_msa_pt=False, + auth_scheme=None, **kwargs): params = pymsalruntime.MSALRuntimeAuthParameters(client_id, authority) params.set_requested_scopes(scopes) @@ -178,6 +188,10 @@ params.set_additional_parameter("msal_gui_thread", "true") # Since pymsalruntime 0.8.1 if enable_msa_pt: _enable_msa_pt(params) + if auth_scheme: + params.set_pop_params( + auth_scheme._http_method, auth_scheme._url.netloc, auth_scheme._url.path, + auth_scheme._nonce) for k, v in kwargs.items(): # This can be used to support domain_hint, max_age, etc. if v is not None: params.set_additional_parameter(k, str(v)) @@ -197,6 +211,7 @@ def _acquire_token_silently( authority, client_id, account_id, scopes, claims=None, correlation_id=None, + auth_scheme=None, **kwargs): # For MSA PT scenario where you use the /organizations, yes, # acquireTokenSilently is expected to fail. - Sam Wilson @@ -208,6 +223,10 @@ params.set_requested_scopes(scopes) if claims: params.set_decoded_claims(claims) + if auth_scheme: + params.set_pop_params( + auth_scheme._http_method, auth_scheme._url.netloc, auth_scheme._url.path, + auth_scheme._nonce) for k, v in kwargs.items(): # This can be used to support domain_hint, max_age, etc. if v is not None: params.set_additional_parameter(k, str(v)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.25.0/msal.egg-info/PKG-INFO new/msal-1.26.0/msal.egg-info/PKG-INFO --- old/msal-1.25.0/msal.egg-info/PKG-INFO 2023-11-04 01:14:35.000000000 +0100 +++ new/msal-1.26.0/msal.egg-info/PKG-INFO 2023-12-05 10:08:03.000000000 +0100 @@ -1,7 +1,7 @@ Metadata-Version: 2.1 Name: msal -Version: 1.25.0 -Summary: The Microsoft Authentication Library (MSAL) for Python library +Version: 1.26.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 Author-email: [email protected] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.25.0/msal.egg-info/SOURCES.txt new/msal-1.26.0/msal.egg-info/SOURCES.txt --- old/msal-1.25.0/msal.egg-info/SOURCES.txt 2023-11-04 01:14:35.000000000 +0100 +++ new/msal-1.26.0/msal.egg-info/SOURCES.txt 2023-12-05 10:08:03.000000000 +0100 @@ -5,6 +5,7 @@ msal/__init__.py msal/__main__.py msal/application.py +msal/auth_scheme.py msal/authority.py msal/broker.py msal/cloudshell.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.25.0/setup.cfg new/msal-1.26.0/setup.cfg --- old/msal-1.25.0/setup.cfg 2023-11-04 01:14:35.341285200 +0100 +++ new/msal-1.26.0/setup.cfg 2023-12-05 10:08:03.082436000 +0100 @@ -4,12 +4,7 @@ [metadata] name = msal version = attr: msal.__version__ -description = - 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. +description = 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. long_description = file: README.md long_description_content_type = text/markdown license = MIT diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.25.0/tests/test_e2e.py new/msal-1.26.0/tests/test_e2e.py --- old/msal-1.25.0/tests/test_e2e.py 2023-11-04 01:14:20.000000000 +0100 +++ new/msal-1.26.0/tests/test_e2e.py 2023-12-05 10:07:48.000000000 +0100 @@ -1,4 +1,4 @@ -"""If the following ENV VAR are available, many end-to-end test cases would run. +"""If the following ENV VAR were available, many end-to-end test cases would run. LAB_APP_CLIENT_SECRET=... LAB_OBO_CLIENT_SECRET=... LAB_APP_CLIENT_ID=... @@ -27,10 +27,23 @@ import msal from tests.http_client import MinimalHttpClient, MinimalResponse from msal.oauth2cli import AuthCodeReceiver +from msal.oauth2cli.oidc import decode_part +try: + import pymsalruntime + broker_available = True +except ImportError: + broker_available = False logger = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG if "-v" in sys.argv else logging.INFO) +try: + from dotenv import load_dotenv # Use this only in local dev machine + load_dotenv() # take environment variables from .env. +except ImportError: + logger.warn("Run pip install -r requirements.txt for optional dependency") + +_AZURE_CLI = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" def _get_app_and_auth_code( client_id, @@ -93,7 +106,7 @@ assertion() def assertCacheWorksForUser( - self, result_from_wire, scope, username=None, data=None): + self, result_from_wire, scope, username=None, data=None, auth_scheme=None): logger.debug( "%s: cache = %s, id_token_claims = %s", self.id(), @@ -109,35 +122,34 @@ set(scope) <= set(result_from_wire["scope"].split(" ")) ): # Going to test acquire_token_silent(...) to locate an AT from cache - result_from_cache = self.app.acquire_token_silent( - scope, account=account, data=data or {}) - self.assertIsNotNone(result_from_cache) + silent_result = self.app.acquire_token_silent( + scope, account=account, data=data or {}, auth_scheme=auth_scheme) + self.assertIsNotNone(silent_result) self.assertIsNone( - result_from_cache.get("refresh_token"), "A cache hit returns no RT") - self.assertEqual( - result_from_wire['access_token'], result_from_cache['access_token'], - "We should get a cached AT") + silent_result.get("refresh_token"), "acquire_token_silent() should return no RT") + if auth_scheme: + self.assertNotEqual( + self.app._TOKEN_SOURCE_CACHE, silent_result[self.app._TOKEN_SOURCE]) + else: + self.assertEqual( + self.app._TOKEN_SOURCE_CACHE, silent_result[self.app._TOKEN_SOURCE]) if "refresh_token" in result_from_wire: + assert auth_scheme is None # Going to test acquire_token_silent(...) to obtain an AT by a RT from cache self.app.token_cache._cache["AccessToken"] = {} # A hacky way to clear ATs - result_from_cache = self.app.acquire_token_silent( - scope, account=account, data=data or {}) - if "refresh_token" not in result_from_wire: - self.assertEqual( - result_from_cache["access_token"], result_from_wire["access_token"], - "The previously cached AT should be returned") - self.assertIsNotNone(result_from_cache, + silent_result = self.app.acquire_token_silent( + scope, account=account, data=data or {}) + self.assertIsNotNone(silent_result, "We should get a result from acquire_token_silent(...) call") - self.assertIsNotNone( - # We used to assert it this way: - # result_from_wire['access_token'] != result_from_cache['access_token'] - # but ROPC in B2C tends to return the same AT we obtained seconds ago. - # Now looking back, "refresh_token grant would return a brand new AT" - # was just an empirical observation but never a commitment in specs, - # so we adjust our way to assert here. - (result_from_cache or {}).get("access_token"), - "We should get an AT from acquire_token_silent(...) call") + self.assertEqual( + # We used to assert it this way: + # result_from_wire['access_token'] != silent_result['access_token'] + # but ROPC in B2C tends to return the same AT we obtained seconds ago. + # Now looking back, "refresh_token grant would return a brand new AT" + # was just an empirical observation but never a commitment in specs, + # so we adjust our way to assert here. + self.app._TOKEN_SOURCE_IDP, silent_result[self.app._TOKEN_SOURCE]) def assertCacheWorksForApp(self, result_from_wire, scope): logger.debug( @@ -150,11 +162,9 @@ self.app.acquire_token_silent(scope, account=None), "acquire_token_silent(..., account=None) shall always return None") # Going to test acquire_token_for_client(...) to locate an AT from cache - result_from_cache = self.app.acquire_token_for_client(scope) - self.assertIsNotNone(result_from_cache) - self.assertEqual( - result_from_wire['access_token'], result_from_cache['access_token'], - "We should get a cached AT") + silent_result = self.app.acquire_token_for_client(scope) + self.assertIsNotNone(silent_result) + self.assertEqual(self.app._TOKEN_SOURCE_CACHE, silent_result[self.app._TOKEN_SOURCE]) @classmethod def _build_app(cls, @@ -192,6 +202,7 @@ client_secret=None, # Since MSAL 1.11, confidential client has ROPC too azure_region=None, http_client=None, + auth_scheme=None, **ignored): assert authority and client_id and username and password and scope self.app = self._build_app( @@ -203,12 +214,14 @@ self.assertEqual( self.app.get_accounts(username=username), [], "Cache starts empty") result = self.app.acquire_token_by_username_password( - username, password, scopes=scope) + username, password, scopes=scope, auth_scheme=auth_scheme) self.assertLoosely(result) self.assertCacheWorksForUser( result, scope, username=username, # Our implementation works even when "profile" scope was not requested, or when profile claims is unavailable in B2C + auth_scheme=auth_scheme, ) + return result @unittest.skipIf( os.getenv("TRAVIS"), # It is set when running on TravisCI or Github Actions @@ -246,6 +259,7 @@ data=None, # Needed by ssh-cert feature prompt=None, enable_msa_passthrough=None, + auth_scheme=None, **ignored): assert client_id and authority and scope self.app = self._build_app(client_id, authority=authority) @@ -266,6 +280,7 @@ </ol></body></html>""".format(id=self.id(), hint=_get_hint( html_mode=True, username=username, lab_name=lab_name, username_uri=username_uri)), + auth_scheme=auth_scheme, data=data or {}, ) self.assertIn( @@ -279,7 +294,8 @@ username, result["id_token_claims"]["preferred_username"], "You are expected to sign in as account {}, but tokens returned is for {}".format( username, result["id_token_claims"]["preferred_username"])) - self.assertCacheWorksForUser(result, scope, username=None, data=data or {}) + self.assertCacheWorksForUser( + result, scope, username=None, data=data or {}, auth_scheme=auth_scheme) return result # For further testing @@ -1147,5 +1163,117 @@ # If this test case passes without exception, # it means MSAL Python is not affected by that. + [email protected](broker_available, "AT POP feature is only supported by using broker") +class PopTestCase(LabBasedTestCase): + def test_at_pop_should_contain_pop_scheme_content(self): + auth_scheme = msal.PopAuthScheme( + http_method=msal.PopAuthScheme.HTTP_GET, + url="https://www.Contoso.com/Path1/Path2?queryParam1=a&queryParam2=b", + nonce="placeholder", + ) + result = self._test_acquire_token_interactive( + # Lab test users tend to get kicked out from WAM, we use local user to test + client_id=_AZURE_CLI, + authority="https://login.microsoftonline.com/organizations", + scope=["https://management.azure.com/.default"], + auth_scheme=auth_scheme, + ) # It also tests assertCacheWorksForUser() + self.assertEqual(result["token_source"], "broker", "POP is only supported by broker") + self.assertEqual(result["token_type"], "pop") + payload = json.loads(decode_part(result["access_token"].split(".")[1])) + logger.debug("AT POP payload = %s", json.dumps(payload, indent=2)) + self.assertEqual(payload["m"], auth_scheme._http_method) + self.assertEqual(payload["u"], auth_scheme._url.netloc) + self.assertEqual(payload["p"], auth_scheme._url.path) + self.assertEqual(payload["nonce"], auth_scheme._nonce) + + # TODO: Remove this, as ROPC support is removed by Broker-on-Win + def test_at_pop_via_testingsts_service(self): + """Based on https://testingsts.azurewebsites.net/ServerNonce""" + self.skipTest("ROPC support is removed by Broker-on-Win") + auth_scheme = msal.PopAuthScheme( + http_method="POST", + url="https://www.Contoso.com/Path1/Path2?queryParam1=a&queryParam2=b", + nonce=requests.get( + # TODO: Could use ".../missing" and then parse its WWW-Authenticate header + "https://testingsts.azurewebsites.net/servernonce/get").text, + ) + config = self.get_lab_user(usertype="cloud") + config["password"] = self.get_lab_user_secret(config["lab_name"]) + result = self._test_username_password(auth_scheme=auth_scheme, **config) + self.assertEqual(result["token_type"], "pop") + shr = result["access_token"] + payload = json.loads(decode_part(result["access_token"].split(".")[1])) + logger.debug("AT POP payload = %s", json.dumps(payload, indent=2)) + self.assertEqual(payload["m"], auth_scheme._http_method) + self.assertEqual(payload["u"], auth_scheme._url.netloc) + self.assertEqual(payload["p"], auth_scheme._url.path) + self.assertEqual(payload["nonce"], auth_scheme._nonce) + + validation = requests.post( + # TODO: This endpoint does not seem to validate the url + "https://testingsts.azurewebsites.net/servernonce/validateshr", + data={"SHR": shr}, + ) + self.assertEqual(validation.status_code, 200) + + def test_at_pop_calling_pattern(self): + # The calling pattern was described here: + # https://identitydivision.visualstudio.com/DevEx/_git/AuthLibrariesApiReview?path=/PoPTokensProtocol/PoP_API_In_MSAL.md&_a=preview&anchor=proposal-2---optional-isproofofposessionsupportedbyclient-helper-(accepted) + + # It is supposed to call app.is_pop_supported() first, + # and then fallback to bearer token code path. + # We skip it here because this test case has not yet initialize self.app + # assert self.app.is_pop_supported() + api_endpoint = "https://20.190.132.47/beta/me" + resp = requests.get(api_endpoint, verify=False) + self.assertEqual(resp.status_code, 401, "Initial call should end with an http 401 error") + result = self._get_shr_pop(**dict( + self.get_lab_user(usertype="cloud"), # This is generally not the current laptop's default AAD account + scope=["https://graph.microsoft.com/.default"], + auth_scheme=msal.PopAuthScheme( + http_method=msal.PopAuthScheme.HTTP_GET, + url=api_endpoint, + nonce=self._extract_pop_nonce(resp.headers.get("WWW-Authenticate")), + ), + )) + resp = requests.get(api_endpoint, verify=False, headers={ + "Authorization": "pop {}".format(result["access_token"]), + }) + if resp.status_code != 200: + # TODO https://teams.microsoft.com/l/message/19:[email protected]/1700184847801?context=%7B%22contextType%22%3A%22chat%22%7D + self.skipTest("We haven't got this end-to-end test case working") + self.assertEqual(resp.status_code, 200, "POP resource should be accessible") + + def _extract_pop_nonce(self, www_authenticate): + # This is a hack for testing purpose only. Do not use this in prod. + # FYI: There is a www-authenticate package but it falters when encountering realm="" + import re + found = re.search(r'nonce="(.+?)"', www_authenticate) + if found: + return found.group(1) + + def _get_shr_pop( + self, client_id=None, authority=None, scope=None, auth_scheme=None, + **kwargs): + result = self._test_acquire_token_interactive( + # Lab test users tend to get kicked out from WAM, we use local user to test + client_id=client_id, + authority=authority, + scope=scope, + auth_scheme=auth_scheme, + **kwargs) # It also tests assertCacheWorksForUser() + self.assertEqual(result["token_source"], "broker", "POP is only supported by broker") + self.assertEqual(result["token_type"], "pop") + payload = json.loads(decode_part(result["access_token"].split(".")[1])) + logger.debug("AT POP payload = %s", json.dumps(payload, indent=2)) + self.assertEqual(payload["m"], auth_scheme._http_method) + self.assertEqual(payload["u"], auth_scheme._url.netloc) + self.assertEqual(payload["p"], auth_scheme._url.path) + self.assertEqual(payload["nonce"], auth_scheme._nonce) + return result + + if __name__ == "__main__": unittest.main()
