1313
1414from .oauth2cli import Client , JwtAssertionCreator
1515from .oauth2cli .oidc import decode_part
16- from .authority import Authority
16+ from .authority import Authority , WORLD_WIDE
1717from .mex import send_request as mex_send_request
1818from .wstrust_request import send_request as wst_send_request
1919from .wstrust_response import *
2525
2626
2727# The __init__.py will import this. Not the other way around.
28- __version__ = "1.18 .0" # When releasing, also check and bump our dependencies's versions if needed
28+ __version__ = "1.19 .0" # When releasing, also check and bump our dependencies's versions if needed
2929
3030logger = logging .getLogger (__name__ )
3131_AUTHORITY_TYPE_CLOUDSHELL = "CLOUDSHELL"
@@ -146,7 +146,6 @@ def obtain_token_by_username_password(self, username, password, **kwargs):
146146
147147
148148class ClientApplication (object ):
149-
150149 ACQUIRE_TOKEN_SILENT_ID = "84"
151150 ACQUIRE_TOKEN_BY_REFRESH_TOKEN = "85"
152151 ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID = "301"
@@ -174,6 +173,7 @@ def __init__(
174173 # when we would eventually want to add this feature to PCA in future.
175174 exclude_scopes = None ,
176175 http_cache = None ,
176+ instance_discovery = None ,
177177 ):
178178 """Create an instance of application.
179179
@@ -300,7 +300,7 @@ def __init__(
300300 Client capability is implemented using "claims" parameter on the wire,
301301 for now.
302302 MSAL will combine them into
303- `claims parameter <https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter`_
303+ `claims parameter <https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter> `_
304304 which you will later provide via one of the acquire-token request.
305305
306306 :param str azure_region:
@@ -409,11 +409,40 @@ def __init__(
409409 Personally Identifiable Information (PII). Encryption is unnecessary.
410410
411411 New in version 1.16.0.
412+
413+ :param boolean instance_discovery:
414+ Historically, MSAL would connect to a central endpoint located at
415+ ``https://login.microsoftonline.com`` to acquire some metadata,
416+ especially when using an unfamiliar authority.
417+ This behavior is known as Instance Discovery.
418+
419+ This parameter defaults to None, which enables the Instance Discovery.
420+
421+ If you know some authorities which you allow MSAL to operate with as-is,
422+ without involving any Instance Discovery, the recommended pattern is::
423+
424+ known_authorities = frozenset([ # Treat your known authorities as const
425+ "https://contoso.com/adfs", "https://login.azs/foo"])
426+ ...
427+ authority = "https://contoso.com/adfs" # Assuming your app will use this
428+ app1 = PublicClientApplication(
429+ "client_id",
430+ authority=authority,
431+ # Conditionally disable Instance Discovery for known authorities
432+ instance_discovery=authority not in known_authorities,
433+ )
434+
435+ If you do not know some authorities beforehand,
436+ yet still want MSAL to accept any authority that you will provide,
437+ you can use a ``False`` to unconditionally disable Instance Discovery.
438+
439+ New in version 1.19.0.
412440 """
413441 self .client_id = client_id
414442 self .client_credential = client_credential
415443 self .client_claims = client_claims
416444 self ._client_capabilities = client_capabilities
445+ self ._instance_discovery = instance_discovery
417446
418447 if exclude_scopes and not isinstance (exclude_scopes , list ):
419448 raise ValueError (
@@ -453,18 +482,24 @@ def __init__(
453482
454483 # Here the self.authority will not be the same type as authority in input
455484 try :
485+ authority_to_use = authority or "https://{}/common/" .format (WORLD_WIDE )
456486 self .authority = Authority (
457- authority or "https://login.microsoftonline.com/common/" ,
458- self .http_client , validate_authority = validate_authority )
487+ authority_to_use ,
488+ self .http_client ,
489+ validate_authority = validate_authority ,
490+ instance_discovery = self ._instance_discovery ,
491+ )
459492 except ValueError : # Those are explicit authority validation errors
460493 raise
461494 except Exception : # The rest are typically connection errors
462495 if validate_authority and azure_region :
463496 # Since caller opts in to use region, here we tolerate connection
464497 # errors happened during authority validation at non-region endpoint
465498 self .authority = Authority (
466- authority or "https://login.microsoftonline.com/common/" ,
467- self .http_client , validate_authority = False )
499+ authority_to_use ,
500+ self .http_client ,
501+ instance_discovery = False ,
502+ )
468503 else :
469504 raise
470505
@@ -526,16 +561,19 @@ def _get_regional_authority(self, central_authority):
526561 if region_to_use :
527562 regional_host = ("{}.r.login.microsoftonline.com" .format (region_to_use )
528563 if central_authority .instance in (
529- # The list came from https://github.com/AzureAD/microsoft-authentication-library-for-python/pull/358/files#r629400328
564+ # The list came from point 3 of the algorithm section in this internal doc
565+ # https://identitydivision.visualstudio.com/DevEx/_git/AuthLibrariesApiReview?path=/PinAuthToRegion/AAD%20SDK%20Proposal%20to%20Pin%20Auth%20to%20region.md&anchor=algorithm&_a=preview
530566 "login.microsoftonline.com" ,
567+ "login.microsoft.com" ,
531568 "login.windows.net" ,
532569 "sts.windows.net" ,
533570 )
534571 else "{}.{}" .format (region_to_use , central_authority .instance ))
535- return Authority (
572+ return Authority ( # The central_authority has already been validated
536573 "https://{}/{}" .format (regional_host , central_authority .tenant ),
537574 self .http_client ,
538- validate_authority = False ) # The central_authority has already been validated
575+ instance_discovery = False ,
576+ )
539577 return None
540578
541579 def _build_client (self , client_credential , authority , skip_regional_client = False ):
@@ -787,7 +825,8 @@ def get_authorization_request_url(
787825 # Multi-tenant app can use new authority on demand
788826 the_authority = Authority (
789827 authority ,
790- self .http_client
828+ self .http_client ,
829+ instance_discovery = self ._instance_discovery ,
791830 ) if authority else self .authority
792831
793832 client = _ClientWithCcsRoutingInfo (
@@ -1010,14 +1049,23 @@ def _find_msal_accounts(self, environment):
10101049 }
10111050 return list (grouped_accounts .values ())
10121051
1052+ def _get_instance_metadata (self ): # This exists so it can be mocked in unit test
1053+ resp = self .http_client .get (
1054+ "https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize" , # TBD: We may extend this to use self._instance_discovery endpoint
1055+ headers = {'Accept' : 'application/json' })
1056+ resp .raise_for_status ()
1057+ return json .loads (resp .text )['metadata' ]
1058+
10131059 def _get_authority_aliases (self , instance ):
1060+ if self ._instance_discovery is False :
1061+ return []
1062+ if self .authority ._is_known_to_developer :
1063+ # Then it is an ADFS/B2C/known_authority_hosts situation
1064+ # which may not reach the central endpoint, so we skip it.
1065+ return []
10141066 if not self .authority_groups :
1015- resp = self .http_client .get (
1016- "https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize" ,
1017- headers = {'Accept' : 'application/json' })
1018- resp .raise_for_status ()
10191067 self .authority_groups = [
1020- set (group ['aliases' ]) for group in json . loads ( resp . text )[ 'metadata' ] ]
1068+ set (group ['aliases' ]) for group in self . _get_instance_metadata () ]
10211069 for group in self .authority_groups :
10221070 if instance in group :
10231071 return [alias for alias in group if alias != instance ]
@@ -1166,6 +1214,7 @@ def acquire_token_silent_with_error(
11661214 # the_authority = Authority(
11671215 # authority,
11681216 # self.http_client,
1217+ # instance_discovery=self._instance_discovery,
11691218 # ) if authority else self.authority
11701219 result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
11711220 scopes , account , self .authority , force_refresh = force_refresh ,
@@ -1187,7 +1236,8 @@ def acquire_token_silent_with_error(
11871236 the_authority = Authority (
11881237 "https://" + alias + "/" + self .authority .tenant ,
11891238 self .http_client ,
1190- validate_authority = False )
1239+ instance_discovery = False ,
1240+ )
11911241 result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
11921242 scopes , account , the_authority , force_refresh = force_refresh ,
11931243 claims_challenge = claims_challenge ,
@@ -1340,7 +1390,7 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
13401390 reverse = True ):
13411391 logger .debug ("Cache attempts an RT" )
13421392 headers = telemetry_context .generate_headers ()
1343- if "home_account_id" in query : # Then use it as CCS Routing info
1393+ if query . get ( "home_account_id" ) : # Then use it as CCS Routing info
13441394 headers ["X-AnchorMailbox" ] = "Oid:{}" .format ( # case-insensitive value
13451395 query ["home_account_id" ].replace ("." , "@" ))
13461396 response = client .obtain_token_by_refresh_token (
0 commit comments