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