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-04-12 15:49:31
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-msal (Old)
 and      /work/SRC/openSUSE:Factory/.python-msal.new.2401 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-msal"

Mon Apr 12 15:49:31 2021 rev:7 rq:881988 version:1.10.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-msal/python-msal.changes  2021-01-22 
21:50:53.633727384 +0100
+++ /work/SRC/openSUSE:Factory/.python-msal.new.2401/python-msal.changes        
2021-04-12 15:49:43.449261928 +0200
@@ -1,0 +2,24 @@
+Thu Mar 25 13:35:20 UTC 2021 - John Paul Adrian Glaubitz 
<adrian.glaub...@suse.com>
+
+- Update to version 1.10.0
+  + Enhancement: Proactive access token (AT) refreshing. Previously, an AT is
+    either valid or expired. If an AT expires and your network happens to have
+    a glitch, your app wouldn't be able to auth. Now, MSAL Python attempts to
+    refresh some AT (typically long-lived AT) half way towards their 
expiration,
+    and silently ignores the error and retries next time, so that your app 
would
+    be more resilient. All these happen automatically, without any code change
+    to your app. (#176, #312, #320)
+  + Adjustment: MSAL Python will keep RT in token cache even when its usage
+    encounters an "invalid_grant" error, so that the RT would likely still
+    be used by other requests. (#314, #315)
+- from version 1.9.0
+  + Enhancement: Starting from this version, MSAL will be compatible with both
+    PyJWT 1.x and PyJWT 2.x (#293, #296)
+  + Enhancement: Better support for upcoming Azure CLI's SSH extension (#300, 
#298)
+  + Enhancement: Better deprecation message for get_authorization_request_url()
+    and acquire_token_by_authorization_code(). (#301, #303)
+  + Enhancement: Better exception message when using incorrect case in 
client_id.
+    (#304, #307)
+  + Other improvements.
+
+-------------------------------------------------------------------

Old:
----
  msal-1.8.0.tar.gz

New:
----
  msal-1.10.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-msal.spec ++++++
--- /var/tmp/diff_new_pack.ty1Bxd/_old  2021-04-12 15:49:43.981262573 +0200
+++ /var/tmp/diff_new_pack.ty1Bxd/_new  2021-04-12 15:49:43.981262573 +0200
@@ -21,7 +21,7 @@
 %define skip_python2 1
 %endif
 Name:           python-msal
-Version:        1.8.0
+Version:        1.10.0
 Release:        0
 Summary:        Microsoft Authentication Library (MSAL) for Python
 License:        MIT

++++++ msal-1.8.0.tar.gz -> msal-1.10.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.8.0/PKG-INFO new/msal-1.10.0/PKG-INFO
--- old/msal-1.8.0/PKG-INFO     2020-12-16 05:52:45.000000000 +0100
+++ new/msal-1.10.0/PKG-INFO    2021-03-08 21:46:32.805985200 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: msal
-Version: 1.8.0
+Version: 1.10.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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.8.0/msal/application.py 
new/msal-1.10.0/msal/application.py
--- old/msal-1.8.0/msal/application.py  2020-12-16 05:52:23.000000000 +0100
+++ new/msal-1.10.0/msal/application.py 2021-03-08 21:46:19.000000000 +0100
@@ -21,7 +21,7 @@
 
 
 # The __init__.py will import this. Not the other way around.
-__version__ = "1.8.0"
+__version__ = "1.10.0"
 
 logger = logging.getLogger(__name__)
 
@@ -56,7 +56,9 @@
 CLIENT_CURRENT_TELEMETRY = 'x-client-current-telemetry'
 
 def _get_new_correlation_id():
-    return str(uuid.uuid4())
+    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):
@@ -98,6 +100,12 @@
         return raw
 
 
+def _clean_up(result):
+    if isinstance(result, dict):
+        result.pop("refresh_in", None)  # MSAL handled refresh_in, customers 
need not
+    return result
+
+
 class ClientApplication(object):
 
     ACQUIRE_TOKEN_SILENT_ID = "84"
@@ -439,16 +447,20 @@
             {"authorization_endpoint": the_authority.authorization_endpoint},
             self.client_id,
             http_client=self.http_client)
-        return client.build_auth_request_uri(
-            response_type=response_type,
-            redirect_uri=redirect_uri, state=state, login_hint=login_hint,
-            prompt=prompt,
-            scope=decorate_scope(scopes, self.client_id),
-            nonce=nonce,
-            domain_hint=domain_hint,
-            claims=_merge_claims_challenge_and_capabilities(
-                self._client_capabilities, claims_challenge),
-            )
+        warnings.warn(
+            "Change your get_authorization_request_url() "
+            "to initiate_auth_code_flow()", DeprecationWarning)
+        with warnings.catch_warnings(record=True):
+            return client.build_auth_request_uri(
+                response_type=response_type,
+                redirect_uri=redirect_uri, state=state, login_hint=login_hint,
+                prompt=prompt,
+                scope=decorate_scope(scopes, self.client_id),
+                nonce=nonce,
+                domain_hint=domain_hint,
+                claims=_merge_claims_challenge_and_capabilities(
+                    self._client_capabilities, claims_challenge),
+                )
 
     def acquire_token_by_auth_code_flow(
             self, auth_code_flow, auth_response, scopes=None, **kwargs):
@@ -501,7 +513,7 @@
                     return redirect(url_for("index"))
         """
         self._validate_ssh_cert_input_data(kwargs.get("data", {}))
-        return self.client.obtain_token_by_auth_code_flow(
+        return _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,
@@ -515,7 +527,7 @@
                 claims=_merge_claims_challenge_and_capabilities(
                     self._client_capabilities,
                     auth_code_flow.pop("claims_challenge", None))),
-            **kwargs)
+            **kwargs))
 
     def acquire_token_by_authorization_code(
             self,
@@ -570,20 +582,24 @@
         # really empty.
         assert isinstance(scopes, list), "Invalid parameter type"
         self._validate_ssh_cert_input_data(kwargs.get("data", {}))
-        return self.client.obtain_token_by_authorization_code(
-            code, redirect_uri=redirect_uri,
-            scope=decorate_scope(scopes, self.client_id),
-            headers={
-                CLIENT_REQUEST_ID: _get_new_correlation_id(),
-                CLIENT_CURRENT_TELEMETRY: 
_build_current_telemetry_request_header(
-                    self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID),
-                },
-            data=dict(
-                kwargs.pop("data", {}),
-                claims=_merge_claims_challenge_and_capabilities(
-                    self._client_capabilities, claims_challenge)),
-            nonce=nonce,
-            **kwargs)
+        warnings.warn(
+            "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(
+                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),
+                    },
+                data=dict(
+                    kwargs.pop("data", {}),
+                    claims=_merge_claims_challenge_and_capabilities(
+                        self._client_capabilities, claims_challenge)),
+                nonce=nonce,
+                **kwargs))
 
     def get_accounts(self, username=None):
         """Get a list of accounts which previously signed in, i.e. exists in 
cache.
@@ -812,6 +828,7 @@
             force_refresh=False,  # type: Optional[boolean]
             claims_challenge=None,
             **kwargs):
+        access_token_from_cache = None
         if not (force_refresh or claims_challenge):  # Bypass AT when desired 
or using claims
             query={
                     "client_id": self.client_id,
@@ -829,17 +846,27 @@
             now = time.time()
             for entry in matches:
                 expires_in = int(entry["expires_on"]) - now
-                if expires_in < 5*60:
+                if expires_in < 5*60:  # Then consider it expired
                     continue  # Removal is not necessary, it will be 
overwritten
                 logger.debug("Cache hit an AT")
-                return {  # Mimic a real response
+                access_token_from_cache = {  # Mimic a real response
                     "access_token": entry["secret"],
                     "token_type": entry.get("token_type", "Bearer"),
                     "expires_in": int(expires_in),  # OAuth2 specs defines it 
as int
                     }
-        return 
self._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
+                if "refresh_on" in entry and int(entry["refresh_on"]) < now:  
# aging
+                    break  # With a fallback in hand, we break here to go 
refresh
+                return access_token_from_cache  # It is still good as new
+        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)
+            if (result and "error" not in result) or (not 
access_token_from_cache):
+                return result
+        except:  # The exact HTTP exception is transportation-layer dependent
+            logger.exception("Refresh token failed")  # Potential AAD outage?
+        return access_token_from_cache
 
     def _acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
             self, authority, scopes, account, **kwargs):
@@ -897,11 +924,17 @@
         client = self._build_client(self.client_credential, authority)
 
         response = None  # A distinguishable value to mean cache is empty
-        for entry in matches:
+        for entry in sorted(  # Since unfit RTs would not be aggressively 
removed,
+                              # we start from newer RTs which are more likely 
fit.
+                matches,
+                key=lambda e: int(e.get("last_modification_time", "0")),
+                reverse=True):
             logger.debug("Cache attempts an RT")
             response = client.obtain_token_by_refresh_token(
                 entry, rt_getter=lambda token_item: token_item["secret"],
-                on_removing_rt=rt_remover or self.token_cache.remove_rt,
+                on_removing_rt=lambda rt_item: None,  # Disable RT removal,
+                    # because an invalid_grant could be caused by new MFA 
policy,
+                    # the RT could still be useful for other MFA-less scope or 
tenant
                 on_obtaining_tokens=lambda event: self.token_cache.add(dict(
                     event,
                     environment=authority.instance,
@@ -942,7 +975,7 @@
                     "you must include a string parameter named 'key_id' "
                     "which identifies the key in the 'req_cnf' argument.")
 
-    def acquire_token_by_refresh_token(self, refresh_token, scopes):
+    def acquire_token_by_refresh_token(self, refresh_token, scopes, **kwargs):
         """Acquire token(s) based on a refresh token (RT) obtained from 
elsewhere.
 
         You use this method only when you have old RTs from elsewhere,
@@ -965,7 +998,8 @@
             * A dict contains "error" and some other keys, when error happened.
             * A dict contains no "error" key means migration was successful.
         """
-        return self.client.obtain_token_by_refresh_token(
+        self._validate_ssh_cert_input_data(kwargs.get("data", {}))
+        return _clean_up(self.client.obtain_token_by_refresh_token(
             refresh_token,
             scope=decorate_scope(scopes, self.client_id),
             headers={
@@ -976,7 +1010,7 @@
             rt_getter=lambda rt: rt,
             on_updating_rt=False,
             on_removing_rt=lambda rt_item: None,  # No OP
-            )
+            **kwargs))
 
 
 class PublicClientApplication(ClientApplication):  # browser app or mobile app
@@ -1002,6 +1036,9 @@
             **kwargs):
         """Acquire token interactively i.e. via a local browser.
 
+        Prerequisite: In Azure Portal, configure the Redirect URI of your
+        "Mobile and Desktop application" as ``http://localhost``.
+
         :param list scope:
             It is a list of case-sensitive strings.
         :param str prompt:
@@ -1050,7 +1087,7 @@
         self._validate_ssh_cert_input_data(kwargs.get("data", {}))
         claims = _merge_claims_challenge_and_capabilities(
             self._client_capabilities, claims_challenge)
-        return self.client.obtain_token_by_browser(
+        return _clean_up(self.client.obtain_token_by_browser(
             scope=decorate_scope(scopes, self.client_id) if scopes else None,
             extra_scope_to_consent=extra_scopes_to_consent,
             redirect_uri="http://localhost:{port}".format(
@@ -1069,7 +1106,7 @@
                 CLIENT_CURRENT_TELEMETRY: 
_build_current_telemetry_request_header(
                     self.ACQUIRE_TOKEN_INTERACTIVE),
                 },
-            **kwargs)
+            **kwargs))
 
     def initiate_device_flow(self, scopes=None, **kwargs):
         """Initiate a Device Flow instance,
@@ -1112,7 +1149,7 @@
             - A successful response would contain "access_token" key,
             - an error response would contain "error" and usually 
"error_description".
         """
-        return self.client.obtain_token_by_device_flow(
+        return _clean_up(self.client.obtain_token_by_device_flow(
             flow,
             data=dict(
                 kwargs.pop("data", {}),
@@ -1128,7 +1165,7 @@
                 CLIENT_CURRENT_TELEMETRY: 
_build_current_telemetry_request_header(
                     self.ACQUIRE_TOKEN_BY_DEVICE_FLOW_ID),
                 },
-            **kwargs)
+            **kwargs))
 
     def acquire_token_by_username_password(
             self, username, password, scopes, claims_challenge=None, **kwargs):
@@ -1166,15 +1203,15 @@
             user_realm_result = self.authority.user_realm_discovery(
                 username, correlation_id=headers[CLIENT_REQUEST_ID])
             if user_realm_result.get("account_type") == "Federated":
-                return self._acquire_token_by_username_password_federated(
+                return 
_clean_up(self._acquire_token_by_username_password_federated(
                     user_realm_result, username, password, scopes=scopes,
                     data=data,
-                    headers=headers, **kwargs)
-        return self.client.obtain_token_by_username_password(
+                    headers=headers, **kwargs))
+        return _clean_up(self.client.obtain_token_by_username_password(
                 username, password, scope=scopes,
                 headers=headers,
                 data=data,
-                **kwargs)
+                **kwargs))
 
     def _acquire_token_by_username_password_federated(
             self, user_realm_result, username, password, scopes=None, 
**kwargs):
@@ -1233,7 +1270,8 @@
             - an error response would contain "error" and usually 
"error_description".
         """
         # TBD: force_refresh behavior
-        return self.client.obtain_token_for_client(
+        self._validate_ssh_cert_input_data(kwargs.get("data", {}))
+        return _clean_up(self.client.obtain_token_for_client(
             scope=scopes,  # This grant flow requires no scope decoration
             headers={
                 CLIENT_REQUEST_ID: _get_new_correlation_id(),
@@ -1244,7 +1282,7 @@
                 kwargs.pop("data", {}),
                 claims=_merge_claims_challenge_and_capabilities(
                     self._client_capabilities, claims_challenge)),
-            **kwargs)
+            **kwargs))
 
     def acquire_token_on_behalf_of(self, user_assertion, scopes, 
claims_challenge=None, **kwargs):
         """Acquires token using on-behalf-of (OBO) flow.
@@ -1274,7 +1312,7 @@
         """
         # The implementation is NOT based on Token Exchange
         # https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16
-        return self.client.obtain_token_by_assertion(  # bases on assertion 
RFC 7521
+        return _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:
@@ -1293,5 +1331,4 @@
                 CLIENT_CURRENT_TELEMETRY: 
_build_current_telemetry_request_header(
                     self.ACQUIRE_TOKEN_ON_BEHALF_OF_ID),
                 },
-            **kwargs)
-
+            **kwargs))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.8.0/msal/oauth2cli/assertion.py 
new/msal-1.10.0/msal/oauth2cli/assertion.py
--- old/msal-1.8.0/msal/oauth2cli/assertion.py  2020-12-16 05:52:23.000000000 
+0100
+++ new/msal-1.10.0/msal/oauth2cli/assertion.py 2021-03-08 21:46:19.000000000 
+0100
@@ -9,6 +9,15 @@
 
 logger = logging.getLogger(__name__)
 
+
+def _str2bytes(raw):
+    # A conversion based on duck-typing rather than six.text_type
+    try:  # Assuming it is a string
+        return raw.encode(encoding="utf-8")
+    except:  # Otherwise we treat it as bytes and return it as-is
+        return raw
+
+
 class AssertionCreator(object):
     def create_normal_assertion(
             self, audience, issuer, subject, expires_at=None, expires_in=600,
@@ -103,8 +112,9 @@
             payload['nbf'] = not_before
         payload.update(additional_claims or {})
         try:
-            return jwt.encode(
+            str_or_bytes = jwt.encode(  # PyJWT 1 returns bytes, PyJWT 2 
returns str
                 payload, self.key, algorithm=self.algorithm, 
headers=self.headers)
+            return _str2bytes(str_or_bytes)  # We normalize them into bytes
         except:
             if self.algorithm.startswith("RS") or 
self.algorithm.starswith("ES"):
                 logger.exception(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.8.0/msal/oauth2cli/authcode.py 
new/msal-1.10.0/msal/oauth2cli/authcode.py
--- old/msal-1.8.0/msal/oauth2cli/authcode.py   2020-12-16 05:52:23.000000000 
+0100
+++ new/msal-1.10.0/msal/oauth2cli/authcode.py  2021-03-08 21:46:19.000000000 
+0100
@@ -33,19 +33,9 @@
             ).get("code")
 
 
-def _browse(auth_uri):
+def _browse(auth_uri):  # throws ImportError, possibly webbrowser.Error in 
future
     import webbrowser  # Lazy import. Some distro may not have this.
-    controller = webbrowser.get()  # Get a default controller
-    # Some Linux Distro does not setup default browser properly,
-    # so we try to explicitly use some popular browser, if we found any.
-    for browser in ["chrome", "firefox", "safari", "windows-default"]:
-        try:
-            controller = webbrowser.get(browser)
-            break
-        except webbrowser.Error:
-            pass  # This browser is not installed. Try next one.
-    logger.info("Please open a browser on THIS device to visit: %s" % auth_uri)
-    controller.open(auth_uri)
+    return webbrowser.open(auth_uri)  # Use default browser. Customizable by 
$BROWSER
 
 
 def _qs2kv(qs):
@@ -130,14 +120,16 @@
         return self._server.server_address[1]
 
     def get_auth_response(self, auth_uri=None, timeout=None, state=None,
-            welcome_template=None, success_template=None, error_template=None):
-        """Wait and return the auth response, or None when timeout.
+            welcome_template=None, success_template=None, error_template=None,
+            auth_uri_callback=None,
+            ):
+        """Wait and return the auth response. Raise RuntimeError when timeout.
 
         :param str auth_uri:
             If provided, this function will try to open a local browser.
         :param int timeout: In seconds. None means wait indefinitely.
         :param str state:
-            You may provide the state you used in auth_url,
+            You may provide the state you used in auth_uri,
             then we will use it to validate incoming response.
         :param str welcome_template:
             If provided, your end user will see it instead of the auth_uri.
@@ -152,6 +144,10 @@
             The page will be displayed when authentication encountered error.
             Placeholders can be any of these:
             https://tools.ietf.org/html/rfc6749#section-5.2
+        :param callable auth_uri_callback:
+            A function with the shape of lambda auth_uri: ...
+            When a browser was unable to be launch, this function will be 
called,
+            so that the app could tell user to manually visit the auth_uri.
         :return:
             The auth response of the first leg of Auth Code flow,
             typically {"code": "...", "state": "..."} or {"error": "...", ...}
@@ -164,8 +160,31 @@
         logger.debug("Abort by visit %s", abort_uri)
         self._server.welcome_page = Template(welcome_template or 
"").safe_substitute(
             auth_uri=auth_uri, abort_uri=abort_uri)
-        if auth_uri:
-            _browse(welcome_uri if welcome_template else auth_uri)
+        if auth_uri:  # Now attempt to open a local browser to visit it
+            _uri = welcome_uri if welcome_template else auth_uri
+            logger.info("Open a browser on this device to visit: %s" % _uri)
+            browser_opened = False
+            try:
+                browser_opened = _browse(_uri)
+            except:  # Had to use broad except, because the potential
+                     # webbrowser.Error is purposely undefined outside of 
_browse().
+                # Absorb and proceed. Because browser could be manually run 
elsewhere.
+                logger.exception("_browse(...) unsuccessful")
+            if not browser_opened:
+                if not auth_uri_callback:
+                    logger.warning(
+                        "Found no browser in current environment. "
+                        "If this program is being run inside a container "
+                        "which has access to host network "
+                        "(i.e. started by `docker run --net=host -it ...`), "
+                        "you can use browser on host to visit the following 
link. "
+                        "Otherwise, this auth attempt would either timeout "
+                        "(current timeout setting is {timeout}) "
+                        "or be aborted by CTRL+C. Auth URI: {auth_uri}".format(
+                            auth_uri=_uri, timeout=timeout))
+                else:  # Then it is the auth_uri_callback()'s job to inform 
the user
+                    auth_uri_callback(_uri)
+
         self._server.success_template = Template(success_template or
             "Authentication completed. You can close this window now.")
         self._server.error_template = Template(error_template or
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.8.0/msal/oauth2cli/oauth2.py 
new/msal-1.10.0/msal/oauth2cli/oauth2.py
--- old/msal-1.8.0/msal/oauth2cli/oauth2.py     2020-12-16 05:52:23.000000000 
+0100
+++ new/msal-1.10.0/msal/oauth2cli/oauth2.py    2021-03-08 21:46:19.000000000 
+0100
@@ -99,8 +99,8 @@
             client_secret (str):  Triggers HTTP AUTH for Confidential Client
             client_assertion (bytes, callable):
                 The client assertion to authenticate this client, per RFC 7521.
-                It can be a raw SAML2 assertion (this method will encode it 
for you),
-                or a raw JWT assertion.
+                It can be a raw SAML2 assertion (we will base64 encode it for 
you),
+                or a raw JWT assertion in bytes (which we will relay to http 
layer).
                 It can also be a callable (recommended),
                 so that we will do lazy creation of an assertion.
             client_assertion_type (str):
@@ -198,7 +198,9 @@
                     self.default_body["client_assertion_type"], lambda a: a)
             _data["client_assertion"] = encoder(
                 self.client_assertion()  # Do lazy on-the-fly computation
-                if callable(self.client_assertion) else self.client_assertion)
+                if callable(self.client_assertion) else self.client_assertion
+                )   # The type is bytes, which is preferrable. See also:
+                    # 
https://github.com/psf/requests/issues/4503#issuecomment-455001070
 
         _data.update(self.default_body)  # It may contain authen parameters
         _data.update(data or {})  # So the content in data param prevails
@@ -578,6 +580,7 @@
             welcome_template=None,
             success_template=None,
             auth_params=None,
+            auth_uri_callback=None,
             **kwargs):
         """A native app can use this method to obtain token via a local 
browser.
 
@@ -635,6 +638,7 @@
                     timeout=timeout,
                     welcome_template=welcome_template,
                     success_template=success_template,
+                    auth_uri_callback=auth_uri_callback,
                     )
         except PermissionError:
             if 0 < listen_port < 1024:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.8.0/msal/oauth2cli/oidc.py 
new/msal-1.10.0/msal/oauth2cli/oidc.py
--- old/msal-1.8.0/msal/oauth2cli/oidc.py       2020-12-16 05:52:23.000000000 
+0100
+++ new/msal-1.10.0/msal/oauth2cli/oidc.py      2021-03-08 21:46:19.000000000 
+0100
@@ -47,7 +47,7 @@
     if _now + skew < decoded.get("nbf", _now - 1):  # nbf is optional per JWT 
specs
         # This is not an ID token validation, but a JWT validation
         # https://tools.ietf.org/html/rfc7519#section-4.1.5
-        err = "0. The ID token is not yet valid"
+        err = "0. The ID token is not yet valid."
     if issuer and issuer != decoded["iss"]:
         # 
https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
         err = ('2. The Issuer Identifier for the OpenID Provider, "%s", '
@@ -57,7 +57,11 @@
         valid_aud = client_id in decoded["aud"] if isinstance(
             decoded["aud"], list) else client_id == decoded["aud"]
         if not valid_aud:
-            err = "3. The aud (audience) Claim must contain this client's 
client_id."
+            err = (
+                "3. The aud (audience) claim must contain this client's 
client_id "
+                '"%s", case-sensitively. Was your client_id in wrong casing?'
+                # Some IdP accepts wrong casing request but issues right 
casing IDT
+                ) % client_id
     # Per specs:
     # 6. If the ID Token is received via direct communication between
     # the Client and the Token Endpoint (which it is during _obtain_token()),
@@ -67,9 +71,9 @@
         err = "9. The current time MUST be before the time represented by the 
exp Claim."
     if nonce and nonce != decoded.get("nonce"):
         err = ("11. Nonce must be the same value "
-            "as the one that was sent in the Authentication Request")
+            "as the one that was sent in the Authentication Request.")
     if err:
-        raise RuntimeError("%s id_token was: %s" % (
+        raise RuntimeError("%s The id_token was: %s" % (
             err, json.dumps(decoded, indent=2)))
     return decoded
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.8.0/msal/token_cache.py 
new/msal-1.10.0/msal/token_cache.py
--- old/msal-1.8.0/msal/token_cache.py  2020-12-16 05:52:23.000000000 +0100
+++ new/msal-1.10.0/msal/token_cache.py 2021-03-08 21:46:19.000000000 +0100
@@ -148,9 +148,9 @@
         target = ' '.join(event.get("scope", []))  # Per schema, we don't sort 
it
 
         with self._lock:
+            now = int(time.time() if now is None else now)
 
             if access_token:
-                now = int(time.time() if now is None else now)
                 expires_in = int(  # AADv1-like endpoint returns a string
                        response.get("expires_in", 3599))
                 ext_expires_in = int(  # AADv1-like endpoint returns a string
@@ -170,6 +170,9 @@
                     }
                 if data.get("key_id"):  # It happens in SSH-cert or POP 
scenario
                     at["key_id"] = data.get("key_id")
+                if "refresh_in" in response:
+                    refresh_in = response["refresh_in"]  # It is an integer
+                    at["refresh_on"] = str(now + refresh_in)  # Schema wants a 
string
                 self.modify(self.CredentialType.ACCESS_TOKEN, at, at)
 
             if client_info and not event.get("skip_account_creation"):
@@ -209,6 +212,7 @@
                     "environment": environment,
                     "client_id": event.get("client_id"),
                     "target": target,  # Optional per schema though
+                    "last_modification_time": str(now),  # Optional. Schema 
defines it as a string.
                     }
                 if "foci" in response:
                     rt["family_id"] = response["foci"]
@@ -246,8 +250,10 @@
 
     def update_rt(self, rt_item, new_rt):
         assert rt_item.get("credential_type") == 
self.CredentialType.REFRESH_TOKEN
-        return self.modify(
-            self.CredentialType.REFRESH_TOKEN, rt_item, {"secret": new_rt})
+        return self.modify(self.CredentialType.REFRESH_TOKEN, rt_item, {
+            "secret": new_rt,
+            "last_modification_time": str(int(time.time())),  # Optional. 
Schema defines it as a string.
+            })
 
     def remove_at(self, at_item):
         assert at_item.get("credential_type") == 
self.CredentialType.ACCESS_TOKEN
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.8.0/msal.egg-info/PKG-INFO 
new/msal-1.10.0/msal.egg-info/PKG-INFO
--- old/msal-1.8.0/msal.egg-info/PKG-INFO       2020-12-16 05:52:45.000000000 
+0100
+++ new/msal-1.10.0/msal.egg-info/PKG-INFO      2021-03-08 21:46:32.000000000 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: msal
-Version: 1.8.0
+Version: 1.10.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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.8.0/msal.egg-info/requires.txt 
new/msal-1.10.0/msal.egg-info/requires.txt
--- old/msal-1.8.0/msal.egg-info/requires.txt   2020-12-16 05:52:45.000000000 
+0100
+++ new/msal-1.10.0/msal.egg-info/requires.txt  2021-03-08 21:46:32.000000000 
+0100
@@ -1,3 +1,3 @@
 requests<3,>=2.0.0
-PyJWT[crypto]<2,>=1.0.0
+PyJWT[crypto]<3,>=1.0.0
 cryptography<4,>=0.6
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.8.0/setup.py new/msal-1.10.0/setup.py
--- old/msal-1.8.0/setup.py     2020-12-16 05:52:23.000000000 +0100
+++ new/msal-1.10.0/setup.py    2021-03-08 21:46:19.000000000 +0100
@@ -73,7 +73,7 @@
         # See https://stackoverflow.com/a/14211600/728675 for more detail
     install_requires=[
         'requests>=2.0.0,<3',
-        'PyJWT[crypto]>=1.0.0,<2',
+        'PyJWT[crypto]>=1.0.0,<3',
 
         'cryptography>=0.6,<4',
             # load_pem_private_key() is available since 0.6

Reply via email to