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()

Reply via email to