2121
2222
2323# The __init__.py will import this. Not the other way around.
24- __version__ = "1.9 .0"
24+ __version__ = "1.10 .0"
2525
2626logger = logging .getLogger (__name__ )
2727
@@ -100,6 +100,12 @@ def _str2bytes(raw):
100100 return raw
101101
102102
103+ def _clean_up (result ):
104+ if isinstance (result , dict ):
105+ result .pop ("refresh_in" , None ) # MSAL handled refresh_in, customers need not
106+ return result
107+
108+
103109class ClientApplication (object ):
104110
105111 ACQUIRE_TOKEN_SILENT_ID = "84"
@@ -507,7 +513,7 @@ def authorize(): # A controller in a web app
507513 return redirect(url_for("index"))
508514 """
509515 self ._validate_ssh_cert_input_data (kwargs .get ("data" , {}))
510- return self .client .obtain_token_by_auth_code_flow (
516+ return _clean_up ( self .client .obtain_token_by_auth_code_flow (
511517 auth_code_flow ,
512518 auth_response ,
513519 scope = decorate_scope (scopes , self .client_id ) if scopes else None ,
@@ -521,7 +527,7 @@ def authorize(): # A controller in a web app
521527 claims = _merge_claims_challenge_and_capabilities (
522528 self ._client_capabilities ,
523529 auth_code_flow .pop ("claims_challenge" , None ))),
524- ** kwargs )
530+ ** kwargs ))
525531
526532 def acquire_token_by_authorization_code (
527533 self ,
@@ -580,7 +586,7 @@ def acquire_token_by_authorization_code(
580586 "Change your acquire_token_by_authorization_code() "
581587 "to acquire_token_by_auth_code_flow()" , DeprecationWarning )
582588 with warnings .catch_warnings (record = True ):
583- return self .client .obtain_token_by_authorization_code (
589+ return _clean_up ( self .client .obtain_token_by_authorization_code (
584590 code , redirect_uri = redirect_uri ,
585591 scope = decorate_scope (scopes , self .client_id ),
586592 headers = {
@@ -593,7 +599,7 @@ def acquire_token_by_authorization_code(
593599 claims = _merge_claims_challenge_and_capabilities (
594600 self ._client_capabilities , claims_challenge )),
595601 nonce = nonce ,
596- ** kwargs )
602+ ** kwargs ))
597603
598604 def get_accounts (self , username = None ):
599605 """Get a list of accounts which previously signed in, i.e. exists in cache.
@@ -822,6 +828,7 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
822828 force_refresh = False , # type: Optional[boolean]
823829 claims_challenge = None ,
824830 ** kwargs ):
831+ access_token_from_cache = None
825832 if not (force_refresh or claims_challenge ): # Bypass AT when desired or using claims
826833 query = {
827834 "client_id" : self .client_id ,
@@ -839,17 +846,27 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
839846 now = time .time ()
840847 for entry in matches :
841848 expires_in = int (entry ["expires_on" ]) - now
842- if expires_in < 5 * 60 :
849+ if expires_in < 5 * 60 : # Then consider it expired
843850 continue # Removal is not necessary, it will be overwritten
844851 logger .debug ("Cache hit an AT" )
845- return { # Mimic a real response
852+ access_token_from_cache = { # Mimic a real response
846853 "access_token" : entry ["secret" ],
847854 "token_type" : entry .get ("token_type" , "Bearer" ),
848855 "expires_in" : int (expires_in ), # OAuth2 specs defines it as int
849856 }
850- return self ._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family (
857+ if "refresh_on" in entry and int (entry ["refresh_on" ]) < now : # aging
858+ break # With a fallback in hand, we break here to go refresh
859+ return access_token_from_cache # It is still good as new
860+ try :
861+ result = self ._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family (
851862 authority , decorate_scope (scopes , self .client_id ), account ,
852863 force_refresh = force_refresh , claims_challenge = claims_challenge , ** kwargs )
864+ result = _clean_up (result )
865+ if (result and "error" not in result ) or (not access_token_from_cache ):
866+ return result
867+ except : # The exact HTTP exception is transportation-layer dependent
868+ logger .exception ("Refresh token failed" ) # Potential AAD outage?
869+ return access_token_from_cache
853870
854871 def _acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family (
855872 self , authority , scopes , account , ** kwargs ):
@@ -907,11 +924,17 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
907924 client = self ._build_client (self .client_credential , authority )
908925
909926 response = None # A distinguishable value to mean cache is empty
910- for entry in matches :
927+ for entry in sorted ( # Since unfit RTs would not be aggressively removed,
928+ # we start from newer RTs which are more likely fit.
929+ matches ,
930+ key = lambda e : int (e .get ("last_modification_time" , "0" )),
931+ reverse = True ):
911932 logger .debug ("Cache attempts an RT" )
912933 response = client .obtain_token_by_refresh_token (
913934 entry , rt_getter = lambda token_item : token_item ["secret" ],
914- on_removing_rt = rt_remover or self .token_cache .remove_rt ,
935+ on_removing_rt = lambda rt_item : None , # Disable RT removal,
936+ # because an invalid_grant could be caused by new MFA policy,
937+ # the RT could still be useful for other MFA-less scope or tenant
915938 on_obtaining_tokens = lambda event : self .token_cache .add (dict (
916939 event ,
917940 environment = authority .instance ,
@@ -976,7 +999,7 @@ def acquire_token_by_refresh_token(self, refresh_token, scopes, **kwargs):
976999 * A dict contains no "error" key means migration was successful.
9771000 """
9781001 self ._validate_ssh_cert_input_data (kwargs .get ("data" , {}))
979- return self .client .obtain_token_by_refresh_token (
1002+ return _clean_up ( self .client .obtain_token_by_refresh_token (
9801003 refresh_token ,
9811004 scope = decorate_scope (scopes , self .client_id ),
9821005 headers = {
@@ -987,7 +1010,7 @@ def acquire_token_by_refresh_token(self, refresh_token, scopes, **kwargs):
9871010 rt_getter = lambda rt : rt ,
9881011 on_updating_rt = False ,
9891012 on_removing_rt = lambda rt_item : None , # No OP
990- ** kwargs )
1013+ ** kwargs ))
9911014
9921015
9931016class PublicClientApplication (ClientApplication ): # browser app or mobile app
@@ -1013,6 +1036,9 @@ def acquire_token_interactive(
10131036 ** kwargs ):
10141037 """Acquire token interactively i.e. via a local browser.
10151038
1039+ Prerequisite: In Azure Portal, configure the Redirect URI of your
1040+ "Mobile and Desktop application" as ``http://localhost``.
1041+
10161042 :param list scope:
10171043 It is a list of case-sensitive strings.
10181044 :param str prompt:
@@ -1061,7 +1087,7 @@ def acquire_token_interactive(
10611087 self ._validate_ssh_cert_input_data (kwargs .get ("data" , {}))
10621088 claims = _merge_claims_challenge_and_capabilities (
10631089 self ._client_capabilities , claims_challenge )
1064- return self .client .obtain_token_by_browser (
1090+ return _clean_up ( self .client .obtain_token_by_browser (
10651091 scope = decorate_scope (scopes , self .client_id ) if scopes else None ,
10661092 extra_scope_to_consent = extra_scopes_to_consent ,
10671093 redirect_uri = "http://localhost:{port}" .format (
@@ -1080,7 +1106,7 @@ def acquire_token_interactive(
10801106 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
10811107 self .ACQUIRE_TOKEN_INTERACTIVE ),
10821108 },
1083- ** kwargs )
1109+ ** kwargs ))
10841110
10851111 def initiate_device_flow (self , scopes = None , ** kwargs ):
10861112 """Initiate a Device Flow instance,
@@ -1123,7 +1149,7 @@ def acquire_token_by_device_flow(self, flow, claims_challenge=None, **kwargs):
11231149 - A successful response would contain "access_token" key,
11241150 - an error response would contain "error" and usually "error_description".
11251151 """
1126- return self .client .obtain_token_by_device_flow (
1152+ return _clean_up ( self .client .obtain_token_by_device_flow (
11271153 flow ,
11281154 data = dict (
11291155 kwargs .pop ("data" , {}),
@@ -1139,7 +1165,7 @@ def acquire_token_by_device_flow(self, flow, claims_challenge=None, **kwargs):
11391165 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
11401166 self .ACQUIRE_TOKEN_BY_DEVICE_FLOW_ID ),
11411167 },
1142- ** kwargs )
1168+ ** kwargs ))
11431169
11441170 def acquire_token_by_username_password (
11451171 self , username , password , scopes , claims_challenge = None , ** kwargs ):
@@ -1177,15 +1203,15 @@ def acquire_token_by_username_password(
11771203 user_realm_result = self .authority .user_realm_discovery (
11781204 username , correlation_id = headers [CLIENT_REQUEST_ID ])
11791205 if user_realm_result .get ("account_type" ) == "Federated" :
1180- return self ._acquire_token_by_username_password_federated (
1206+ return _clean_up ( self ._acquire_token_by_username_password_federated (
11811207 user_realm_result , username , password , scopes = scopes ,
11821208 data = data ,
1183- headers = headers , ** kwargs )
1184- return self .client .obtain_token_by_username_password (
1209+ headers = headers , ** kwargs ))
1210+ return _clean_up ( self .client .obtain_token_by_username_password (
11851211 username , password , scope = scopes ,
11861212 headers = headers ,
11871213 data = data ,
1188- ** kwargs )
1214+ ** kwargs ))
11891215
11901216 def _acquire_token_by_username_password_federated (
11911217 self , user_realm_result , username , password , scopes = None , ** kwargs ):
@@ -1245,7 +1271,7 @@ def acquire_token_for_client(self, scopes, claims_challenge=None, **kwargs):
12451271 """
12461272 # TBD: force_refresh behavior
12471273 self ._validate_ssh_cert_input_data (kwargs .get ("data" , {}))
1248- return self .client .obtain_token_for_client (
1274+ return _clean_up ( self .client .obtain_token_for_client (
12491275 scope = scopes , # This grant flow requires no scope decoration
12501276 headers = {
12511277 CLIENT_REQUEST_ID : _get_new_correlation_id (),
@@ -1256,7 +1282,7 @@ def acquire_token_for_client(self, scopes, claims_challenge=None, **kwargs):
12561282 kwargs .pop ("data" , {}),
12571283 claims = _merge_claims_challenge_and_capabilities (
12581284 self ._client_capabilities , claims_challenge )),
1259- ** kwargs )
1285+ ** kwargs ))
12601286
12611287 def acquire_token_on_behalf_of (self , user_assertion , scopes , claims_challenge = None , ** kwargs ):
12621288 """Acquires token using on-behalf-of (OBO) flow.
@@ -1286,7 +1312,7 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, claims_challenge=No
12861312 """
12871313 # The implementation is NOT based on Token Exchange
12881314 # https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16
1289- return self .client .obtain_token_by_assertion ( # bases on assertion RFC 7521
1315+ return _clean_up ( self .client .obtain_token_by_assertion ( # bases on assertion RFC 7521
12901316 user_assertion ,
12911317 self .client .GRANT_TYPE_JWT , # IDTs and AAD ATs are all JWTs
12921318 scope = decorate_scope (scopes , self .client_id ), # Decoration is used for:
@@ -1305,4 +1331,4 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, claims_challenge=No
13051331 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
13061332 self .ACQUIRE_TOKEN_ON_BEHALF_OF_ID ),
13071333 },
1308- ** kwargs )
1334+ ** kwargs ))
0 commit comments