2323
2424
2525# The __init__.py will import this. Not the other way around.
26- __version__ = "1.12 .0"
26+ __version__ = "1.13 .0"
2727
2828logger = logging .getLogger (__name__ )
2929
@@ -131,6 +131,14 @@ def __init__(
131131 "The provided signature value did not match the expected signature value",
132132 you may try use only the leaf cert (in PEM/str format) instead.
133133
134+ *Added in version 1.13.0*:
135+ It can also be a completly pre-signed assertion that you've assembled yourself.
136+ Simply pass a container containing only the key "client_assertion", like this::
137+
138+ {
139+ "client_assertion": "...a JWT with claims aud, exp, iss, jti, nbf, and sub..."
140+ }
141+
134142 :param dict client_claims:
135143 *Added in version 0.5.0*:
136144 It is a dictionary of extra claims that would be signed by
@@ -377,7 +385,7 @@ def _get_regional_authority(self, central_authority):
377385 validate_authority = False ) # The central_authority has already been validated
378386 return None
379387
380- def _build_client (self , client_credential , authority ):
388+ def _build_client (self , client_credential , authority , skip_regional_client = False ):
381389 client_assertion = None
382390 client_assertion_type = None
383391 default_headers = {
@@ -391,28 +399,32 @@ def _build_client(self, client_credential, authority):
391399 default_headers ['x-app-ver' ] = self .app_version
392400 default_body = {"client_info" : 1 }
393401 if isinstance (client_credential , dict ):
394- assert ("private_key" in client_credential
395- and "thumbprint" in client_credential )
396- headers = {}
397- if 'public_certificate' in client_credential :
398- headers ["x5c" ] = extract_certs (client_credential ['public_certificate' ])
399- if not client_credential .get ("passphrase" ):
400- unencrypted_private_key = client_credential ['private_key' ]
401- else :
402- from cryptography .hazmat .primitives import serialization
403- from cryptography .hazmat .backends import default_backend
404- unencrypted_private_key = serialization .load_pem_private_key (
405- _str2bytes (client_credential ["private_key" ]),
406- _str2bytes (client_credential ["passphrase" ]),
407- backend = default_backend (), # It was a required param until 2020
408- )
409- assertion = JwtAssertionCreator (
410- unencrypted_private_key , algorithm = "RS256" ,
411- sha1_thumbprint = client_credential .get ("thumbprint" ), headers = headers )
412- client_assertion = assertion .create_regenerative_assertion (
413- audience = authority .token_endpoint , issuer = self .client_id ,
414- additional_claims = self .client_claims or {})
402+ assert (("private_key" in client_credential
403+ and "thumbprint" in client_credential ) or
404+ "client_assertion" in client_credential )
415405 client_assertion_type = Client .CLIENT_ASSERTION_TYPE_JWT
406+ if 'client_assertion' in client_credential :
407+ client_assertion = client_credential ['client_assertion' ]
408+ else :
409+ headers = {}
410+ if 'public_certificate' in client_credential :
411+ headers ["x5c" ] = extract_certs (client_credential ['public_certificate' ])
412+ if not client_credential .get ("passphrase" ):
413+ unencrypted_private_key = client_credential ['private_key' ]
414+ else :
415+ from cryptography .hazmat .primitives import serialization
416+ from cryptography .hazmat .backends import default_backend
417+ unencrypted_private_key = serialization .load_pem_private_key (
418+ _str2bytes (client_credential ["private_key" ]),
419+ _str2bytes (client_credential ["passphrase" ]),
420+ backend = default_backend (), # It was a required param until 2020
421+ )
422+ assertion = JwtAssertionCreator (
423+ unencrypted_private_key , algorithm = "RS256" ,
424+ sha1_thumbprint = client_credential .get ("thumbprint" ), headers = headers )
425+ client_assertion = assertion .create_regenerative_assertion (
426+ audience = authority .token_endpoint , issuer = self .client_id ,
427+ additional_claims = self .client_claims or {})
416428 else :
417429 default_body ['client_secret' ] = client_credential
418430 central_configuration = {
@@ -436,7 +448,8 @@ def _build_client(self, client_credential, authority):
436448 on_updating_rt = self .token_cache .update_rt )
437449
438450 regional_client = None
439- if client_credential : # Currently regional endpoint only serves some CCA flows
451+ if (client_credential # Currently regional endpoint only serves some CCA flows
452+ and not skip_regional_client ):
440453 regional_authority = self ._get_regional_authority (authority )
441454 if regional_authority :
442455 regional_configuration = {
@@ -777,7 +790,7 @@ def get_accounts(self, username=None):
777790 accounts = [a for a in accounts
778791 if a ["username" ].lower () == lowercase_username ]
779792 if not accounts :
780- logger .warning ((
793+ logger .debug (( # This would also happen when the cache is empty
781794 "get_accounts(username='{}') finds no account. "
782795 "If tokens were acquired without 'profile' scope, "
783796 "they would contain no username for filtering. "
@@ -1102,9 +1115,13 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
11021115 # target=scopes, # AAD RTs are scope-independent
11031116 query = query )
11041117 logger .debug ("Found %d RTs matching %s" , len (matches ), query )
1105- client , _ = self ._build_client (self .client_credential , authority )
11061118
11071119 response = None # A distinguishable value to mean cache is empty
1120+ if not matches : # Then exit early to avoid expensive operations
1121+ return response
1122+ client , _ = self ._build_client (
1123+ # Potentially expensive if building regional client
1124+ self .client_credential , authority , skip_regional_client = True )
11081125 telemetry_context = self ._build_telemetry_context (
11091126 self .ACQUIRE_TOKEN_SILENT_ID ,
11101127 correlation_id = correlation_id , refresh_reason = refresh_reason )
0 commit comments