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 *
@@ -150,7 +150,6 @@ def obtain_token_by_username_password(self, username, password, **kwargs):
150150
151151
152152class ClientApplication (object ):
153-
154153 ACQUIRE_TOKEN_SILENT_ID = "84"
155154 ACQUIRE_TOKEN_BY_REFRESH_TOKEN = "85"
156155 ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID = "301"
@@ -178,6 +177,7 @@ def __init__(
178177 # when we would eventually want to add this feature to PCA in future.
179178 exclude_scopes = None ,
180179 http_cache = None ,
180+ instance_discovery = None ,
181181 allow_broker = None ,
182182 ):
183183 """Create an instance of application.
@@ -415,6 +415,34 @@ def __init__(
415415
416416 New in version 1.16.0.
417417
418+ :param boolean instance_discovery:
419+ Historically, MSAL would connect to a central endpoint located at
420+ ``https://login.microsoftonline.com`` to acquire some metadata,
421+ especially when using an unfamiliar authority.
422+ This behavior is known as Instance Discovery.
423+
424+ This parameter defaults to None, which enables the Instance Discovery.
425+
426+ If you know some authorities which you allow MSAL to operate with as-is,
427+ without involving any Instance Discovery, the recommended pattern is::
428+
429+ known_authorities = frozenset([ # Treat your known authorities as const
430+ "https://contoso.com/adfs", "https://login.azs/foo"])
431+ ...
432+ authority = "https://contoso.com/adfs" # Assuming your app will use this
433+ app1 = PublicClientApplication(
434+ "client_id",
435+ authority=authority,
436+ # Conditionally disable Instance Discovery for known authorities
437+ instance_discovery=authority not in known_authorities,
438+ )
439+
440+ If you do not know some authorities beforehand,
441+ yet still want MSAL to accept any authority that you will provide,
442+ you can use a ``False`` to unconditionally disable Instance Discovery.
443+
444+ New in version 1.19.0.
445+
418446 :param boolean allow_broker:
419447 Brokers provide Single-Sign-On, device identification,
420448 and application identification verification.
@@ -448,6 +476,7 @@ def __init__(
448476 self .client_credential = client_credential
449477 self .client_claims = client_claims
450478 self ._client_capabilities = client_capabilities
479+ self ._instance_discovery = instance_discovery
451480
452481 if exclude_scopes and not isinstance (exclude_scopes , list ):
453482 raise ValueError (
@@ -487,18 +516,24 @@ def __init__(
487516
488517 # Here the self.authority will not be the same type as authority in input
489518 try :
519+ authority_to_use = authority or "https://{}/common/" .format (WORLD_WIDE )
490520 self .authority = Authority (
491- authority or "https://login.microsoftonline.com/common/" ,
492- self .http_client , validate_authority = validate_authority )
521+ authority_to_use ,
522+ self .http_client ,
523+ validate_authority = validate_authority ,
524+ instance_discovery = self ._instance_discovery ,
525+ )
493526 except ValueError : # Those are explicit authority validation errors
494527 raise
495528 except Exception : # The rest are typically connection errors
496529 if validate_authority and azure_region :
497530 # Since caller opts in to use region, here we tolerate connection
498531 # errors happened during authority validation at non-region endpoint
499532 self .authority = Authority (
500- authority or "https://login.microsoftonline.com/common/" ,
501- self .http_client , validate_authority = False )
533+ authority_to_use ,
534+ self .http_client ,
535+ instance_discovery = False ,
536+ )
502537 else :
503538 raise
504539 is_confidential_app = bool (
@@ -584,10 +619,11 @@ def _get_regional_authority(self, central_authority):
584619 "sts.windows.net" ,
585620 )
586621 else "{}.{}" .format (region_to_use , central_authority .instance ))
587- return Authority (
622+ return Authority ( # The central_authority has already been validated
588623 "https://{}/{}" .format (regional_host , central_authority .tenant ),
589624 self .http_client ,
590- validate_authority = False ) # The central_authority has already been validated
625+ instance_discovery = False ,
626+ )
591627 return None
592628
593629 def _build_client (self , client_credential , authority , skip_regional_client = False ):
@@ -839,7 +875,8 @@ def get_authorization_request_url(
839875 # Multi-tenant app can use new authority on demand
840876 the_authority = Authority (
841877 authority ,
842- self .http_client
878+ self .http_client ,
879+ instance_discovery = self ._instance_discovery ,
843880 ) if authority else self .authority
844881
845882 client = _ClientWithCcsRoutingInfo (
@@ -1062,14 +1099,23 @@ def _find_msal_accounts(self, environment):
10621099 }
10631100 return list (grouped_accounts .values ())
10641101
1102+ def _get_instance_metadata (self ): # This exists so it can be mocked in unit test
1103+ resp = self .http_client .get (
1104+ "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
1105+ headers = {'Accept' : 'application/json' })
1106+ resp .raise_for_status ()
1107+ return json .loads (resp .text )['metadata' ]
1108+
10651109 def _get_authority_aliases (self , instance ):
1110+ if self ._instance_discovery is False :
1111+ return []
1112+ if self .authority ._is_known_to_developer :
1113+ # Then it is an ADFS/B2C/known_authority_hosts situation
1114+ # which may not reach the central endpoint, so we skip it.
1115+ return []
10661116 if not self .authority_groups :
1067- resp = self .http_client .get (
1068- "https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize" ,
1069- headers = {'Accept' : 'application/json' })
1070- resp .raise_for_status ()
10711117 self .authority_groups = [
1072- set (group ['aliases' ]) for group in json . loads ( resp . text )[ 'metadata' ] ]
1118+ set (group ['aliases' ]) for group in self . _get_instance_metadata () ]
10731119 for group in self .authority_groups :
10741120 if instance in group :
10751121 return [alias for alias in group if alias != instance ]
@@ -1223,6 +1269,7 @@ def acquire_token_silent_with_error(
12231269 # the_authority = Authority(
12241270 # authority,
12251271 # self.http_client,
1272+ # instance_discovery=self._instance_discovery,
12261273 # ) if authority else self.authority
12271274 result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
12281275 scopes , account , self .authority , force_refresh = force_refresh ,
@@ -1244,7 +1291,8 @@ def acquire_token_silent_with_error(
12441291 the_authority = Authority (
12451292 "https://" + alias + "/" + self .authority .tenant ,
12461293 self .http_client ,
1247- validate_authority = False )
1294+ instance_discovery = False ,
1295+ )
12481296 result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
12491297 scopes , account , the_authority , force_refresh = force_refresh ,
12501298 claims_challenge = claims_challenge ,
@@ -1539,10 +1587,9 @@ def acquire_token_by_username_password(
15391587 scopes , # Decorated scopes won't work due to offline_access
15401588 MSALRuntime_Username = username ,
15411589 MSALRuntime_Password = password ,
1542- validateAuthority = "no"
1543- if self .authority ._validate_authority is False
1544- or self .authority .is_adfs or self .authority ._is_b2c
1545- else None ,
1590+ validateAuthority = "no" if (
1591+ self .authority ._is_known_to_developer
1592+ or self ._instance_discovery is False ) else None ,
15461593 claims = claims ,
15471594 )
15481595 return self ._process_broker_response (response , scopes , kwargs .get ("data" , {}))
@@ -1807,10 +1854,9 @@ def _acquire_token_interactive_via_broker(
18071854 logger .debug (kwargs ["welcome_template" ]) # Experimental
18081855 authority = "https://{}/{}" .format (
18091856 self .authority .instance , self .authority .tenant )
1810- validate_authority = (
1811- "no" if self .authority ._validate_authority is False
1812- or self .authority .is_adfs or self .authority ._is_b2c
1813- else None )
1857+ validate_authority = "no" if (
1858+ self .authority ._is_known_to_developer
1859+ or self ._instance_discovery is False ) else None
18141860 # Calls different broker methods to mimic the OIDC behaviors
18151861 if login_hint and prompt != "select_account" : # OIDC prompts when the user did not sign in
18161862 accounts = self .get_accounts (username = login_hint )
0 commit comments