21
21
22
22
23
23
# The __init__.py will import this. Not the other way around.
24
- __version__ = "1.9 .0"
24
+ __version__ = "1.10 .0"
25
25
26
26
logger = logging .getLogger (__name__ )
27
27
@@ -100,6 +100,12 @@ def _str2bytes(raw):
100
100
return raw
101
101
102
102
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
+
103
109
class ClientApplication (object ):
104
110
105
111
ACQUIRE_TOKEN_SILENT_ID = "84"
@@ -507,7 +513,7 @@ def authorize(): # A controller in a web app
507
513
return redirect(url_for("index"))
508
514
"""
509
515
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 (
511
517
auth_code_flow ,
512
518
auth_response ,
513
519
scope = decorate_scope (scopes , self .client_id ) if scopes else None ,
@@ -521,7 +527,7 @@ def authorize(): # A controller in a web app
521
527
claims = _merge_claims_challenge_and_capabilities (
522
528
self ._client_capabilities ,
523
529
auth_code_flow .pop ("claims_challenge" , None ))),
524
- ** kwargs )
530
+ ** kwargs ))
525
531
526
532
def acquire_token_by_authorization_code (
527
533
self ,
@@ -580,7 +586,7 @@ def acquire_token_by_authorization_code(
580
586
"Change your acquire_token_by_authorization_code() "
581
587
"to acquire_token_by_auth_code_flow()" , DeprecationWarning )
582
588
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 (
584
590
code , redirect_uri = redirect_uri ,
585
591
scope = decorate_scope (scopes , self .client_id ),
586
592
headers = {
@@ -593,7 +599,7 @@ def acquire_token_by_authorization_code(
593
599
claims = _merge_claims_challenge_and_capabilities (
594
600
self ._client_capabilities , claims_challenge )),
595
601
nonce = nonce ,
596
- ** kwargs )
602
+ ** kwargs ))
597
603
598
604
def get_accounts (self , username = None ):
599
605
"""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(
822
828
force_refresh = False , # type: Optional[boolean]
823
829
claims_challenge = None ,
824
830
** kwargs ):
831
+ access_token_from_cache = None
825
832
if not (force_refresh or claims_challenge ): # Bypass AT when desired or using claims
826
833
query = {
827
834
"client_id" : self .client_id ,
@@ -839,17 +846,27 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
839
846
now = time .time ()
840
847
for entry in matches :
841
848
expires_in = int (entry ["expires_on" ]) - now
842
- if expires_in < 5 * 60 :
849
+ if expires_in < 5 * 60 : # Then consider it expired
843
850
continue # Removal is not necessary, it will be overwritten
844
851
logger .debug ("Cache hit an AT" )
845
- return { # Mimic a real response
852
+ access_token_from_cache = { # Mimic a real response
846
853
"access_token" : entry ["secret" ],
847
854
"token_type" : entry .get ("token_type" , "Bearer" ),
848
855
"expires_in" : int (expires_in ), # OAuth2 specs defines it as int
849
856
}
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 (
851
862
authority , decorate_scope (scopes , self .client_id ), account ,
852
863
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
853
870
854
871
def _acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family (
855
872
self , authority , scopes , account , ** kwargs ):
@@ -907,11 +924,17 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
907
924
client = self ._build_client (self .client_credential , authority )
908
925
909
926
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 ):
911
932
logger .debug ("Cache attempts an RT" )
912
933
response = client .obtain_token_by_refresh_token (
913
934
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
915
938
on_obtaining_tokens = lambda event : self .token_cache .add (dict (
916
939
event ,
917
940
environment = authority .instance ,
@@ -976,7 +999,7 @@ def acquire_token_by_refresh_token(self, refresh_token, scopes, **kwargs):
976
999
* A dict contains no "error" key means migration was successful.
977
1000
"""
978
1001
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 (
980
1003
refresh_token ,
981
1004
scope = decorate_scope (scopes , self .client_id ),
982
1005
headers = {
@@ -987,7 +1010,7 @@ def acquire_token_by_refresh_token(self, refresh_token, scopes, **kwargs):
987
1010
rt_getter = lambda rt : rt ,
988
1011
on_updating_rt = False ,
989
1012
on_removing_rt = lambda rt_item : None , # No OP
990
- ** kwargs )
1013
+ ** kwargs ))
991
1014
992
1015
993
1016
class PublicClientApplication (ClientApplication ): # browser app or mobile app
@@ -1013,6 +1036,9 @@ def acquire_token_interactive(
1013
1036
** kwargs ):
1014
1037
"""Acquire token interactively i.e. via a local browser.
1015
1038
1039
+ Prerequisite: In Azure Portal, configure the Redirect URI of your
1040
+ "Mobile and Desktop application" as ``http://localhost``.
1041
+
1016
1042
:param list scope:
1017
1043
It is a list of case-sensitive strings.
1018
1044
:param str prompt:
@@ -1061,7 +1087,7 @@ def acquire_token_interactive(
1061
1087
self ._validate_ssh_cert_input_data (kwargs .get ("data" , {}))
1062
1088
claims = _merge_claims_challenge_and_capabilities (
1063
1089
self ._client_capabilities , claims_challenge )
1064
- return self .client .obtain_token_by_browser (
1090
+ return _clean_up ( self .client .obtain_token_by_browser (
1065
1091
scope = decorate_scope (scopes , self .client_id ) if scopes else None ,
1066
1092
extra_scope_to_consent = extra_scopes_to_consent ,
1067
1093
redirect_uri = "http://localhost:{port}" .format (
@@ -1080,7 +1106,7 @@ def acquire_token_interactive(
1080
1106
CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
1081
1107
self .ACQUIRE_TOKEN_INTERACTIVE ),
1082
1108
},
1083
- ** kwargs )
1109
+ ** kwargs ))
1084
1110
1085
1111
def initiate_device_flow (self , scopes = None , ** kwargs ):
1086
1112
"""Initiate a Device Flow instance,
@@ -1123,7 +1149,7 @@ def acquire_token_by_device_flow(self, flow, claims_challenge=None, **kwargs):
1123
1149
- A successful response would contain "access_token" key,
1124
1150
- an error response would contain "error" and usually "error_description".
1125
1151
"""
1126
- return self .client .obtain_token_by_device_flow (
1152
+ return _clean_up ( self .client .obtain_token_by_device_flow (
1127
1153
flow ,
1128
1154
data = dict (
1129
1155
kwargs .pop ("data" , {}),
@@ -1139,7 +1165,7 @@ def acquire_token_by_device_flow(self, flow, claims_challenge=None, **kwargs):
1139
1165
CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
1140
1166
self .ACQUIRE_TOKEN_BY_DEVICE_FLOW_ID ),
1141
1167
},
1142
- ** kwargs )
1168
+ ** kwargs ))
1143
1169
1144
1170
def acquire_token_by_username_password (
1145
1171
self , username , password , scopes , claims_challenge = None , ** kwargs ):
@@ -1177,15 +1203,15 @@ def acquire_token_by_username_password(
1177
1203
user_realm_result = self .authority .user_realm_discovery (
1178
1204
username , correlation_id = headers [CLIENT_REQUEST_ID ])
1179
1205
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 (
1181
1207
user_realm_result , username , password , scopes = scopes ,
1182
1208
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 (
1185
1211
username , password , scope = scopes ,
1186
1212
headers = headers ,
1187
1213
data = data ,
1188
- ** kwargs )
1214
+ ** kwargs ))
1189
1215
1190
1216
def _acquire_token_by_username_password_federated (
1191
1217
self , user_realm_result , username , password , scopes = None , ** kwargs ):
@@ -1245,7 +1271,7 @@ def acquire_token_for_client(self, scopes, claims_challenge=None, **kwargs):
1245
1271
"""
1246
1272
# TBD: force_refresh behavior
1247
1273
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 (
1249
1275
scope = scopes , # This grant flow requires no scope decoration
1250
1276
headers = {
1251
1277
CLIENT_REQUEST_ID : _get_new_correlation_id (),
@@ -1256,7 +1282,7 @@ def acquire_token_for_client(self, scopes, claims_challenge=None, **kwargs):
1256
1282
kwargs .pop ("data" , {}),
1257
1283
claims = _merge_claims_challenge_and_capabilities (
1258
1284
self ._client_capabilities , claims_challenge )),
1259
- ** kwargs )
1285
+ ** kwargs ))
1260
1286
1261
1287
def acquire_token_on_behalf_of (self , user_assertion , scopes , claims_challenge = None , ** kwargs ):
1262
1288
"""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
1286
1312
"""
1287
1313
# The implementation is NOT based on Token Exchange
1288
1314
# 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
1290
1316
user_assertion ,
1291
1317
self .client .GRANT_TYPE_JWT , # IDTs and AAD ATs are all JWTs
1292
1318
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
1305
1331
CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
1306
1332
self .ACQUIRE_TOKEN_ON_BEHALF_OF_ID ),
1307
1333
},
1308
- ** kwargs )
1334
+ ** kwargs ))
0 commit comments