22import json
33import time
44try : # Python 2
5- from urlparse import urljoin
5+ from urlparse import urljoin , urlparse
66except : # Python 3
7- from urllib .parse import urljoin
7+ from urllib .parse import urljoin , urlparse
88import logging
99import sys
1010import warnings
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 *
@@ -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"
@@ -160,6 +159,48 @@ class ClientApplication(object):
160159
161160 ATTEMPT_REGION_DISCOVERY = True # "TryAutoDetect"
162161
162+ _known_authority_hosts = None
163+
164+ @classmethod
165+ def set_known_authority_hosts (cls , known_authority_hosts ):
166+ """Declare a list of hosts which you allow MSAL to operate with.
167+
168+ If your app operates with some authorities that you know and own,
169+ such as some ADFS or B2C or private cloud,
170+ it is recommended and sometimes required that you declare them here,
171+ so that MSAL will use your authorities without discovery,
172+ and reject most of the other undefined authorities.
173+
174+ ``known_authority_hosts`` is meant to be a static and per-deployment setting.
175+ This classmethod shall be called at most once,
176+ during your entire app's starting-up,
177+ before your initializing any ``PublicClientApplication`` or
178+ ``ConfidentialClientApplication`` instance(s).
179+
180+ :param list[str] known_authority_hosts:
181+ Authorities that you known, for example::
182+
183+ [
184+ "contoso.com", # Your own domain
185+ "login.azs", # This can be a private cloud
186+ ]
187+
188+ New in version 1.19
189+ """
190+ new_input = frozenset (known_authority_hosts )
191+ if (cls ._known_authority_hosts is not None
192+ and cls ._known_authority_hosts != new_input ):
193+ raise ValueError (
194+ "The known_authority_hosts are considered static. "
195+ "Once configured, they should not be changed." )
196+ cls ._known_authority_hosts = new_input
197+ logger .debug ('known_authority_hosts is set to %s' , known_authority_hosts )
198+
199+ def _union_known_authority_hosts (cls , url = None , host = None ):
200+ host = host if host else urlparse (url ).netloc .split (":" )[0 ]
201+ return (cls ._known_authority_hosts .union ([host ])
202+ if cls ._known_authority_hosts else frozenset ([host ]))
203+
163204 def __init__ (
164205 self , client_id ,
165206 client_credential = None , authority = None , validate_authority = True ,
@@ -453,18 +494,24 @@ def __init__(
453494
454495 # Here the self.authority will not be the same type as authority in input
455496 try :
497+ authority_to_use = authority or "https://{}/common/" .format (WORLD_WIDE )
456498 self .authority = Authority (
457- authority or "https://login.microsoftonline.com/common/" ,
458- self .http_client , validate_authority = validate_authority )
499+ authority_to_use ,
500+ self .http_client , validate_authority = validate_authority ,
501+ known_authority_hosts = self .__class__ ._known_authority_hosts ,
502+ )
459503 except ValueError : # Those are explicit authority validation errors
460504 raise
461505 except Exception : # The rest are typically connection errors
462506 if validate_authority and azure_region :
463507 # Since caller opts in to use region, here we tolerate connection
464508 # errors happened during authority validation at non-region endpoint
465509 self .authority = Authority (
466- authority or "https://login.microsoftonline.com/common/" ,
467- self .http_client , validate_authority = False )
510+ authority_to_use ,
511+ self .http_client ,
512+ known_authority_hosts = self ._union_known_authority_hosts (
513+ url = authority_to_use ),
514+ )
468515 else :
469516 raise
470517
@@ -534,10 +581,12 @@ def _get_regional_authority(self, central_authority):
534581 "sts.windows.net" ,
535582 )
536583 else "{}.{}" .format (region_to_use , central_authority .instance ))
537- return Authority (
584+ return Authority ( # The central_authority has already been validated
538585 "https://{}/{}" .format (regional_host , central_authority .tenant ),
539586 self .http_client ,
540- validate_authority = False ) # The central_authority has already been validated
587+ known_authority_hosts = self ._union_known_authority_hosts (
588+ host = regional_host ),
589+ )
541590 return None
542591
543592 def _build_client (self , client_credential , authority , skip_regional_client = False ):
@@ -789,7 +838,8 @@ def get_authorization_request_url(
789838 # Multi-tenant app can use new authority on demand
790839 the_authority = Authority (
791840 authority ,
792- self .http_client
841+ self .http_client ,
842+ known_authority_hosts = self .__class__ ._known_authority_hosts ,
793843 ) if authority else self .authority
794844
795845 client = _ClientWithCcsRoutingInfo (
@@ -1012,14 +1062,21 @@ def _find_msal_accounts(self, environment):
10121062 }
10131063 return list (grouped_accounts .values ())
10141064
1065+ def _get_instance_metadata (self ): # This exists so it can be mocked in unit test
1066+ resp = self .http_client .get (
1067+ "https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize" ,
1068+ headers = {'Accept' : 'application/json' })
1069+ resp .raise_for_status ()
1070+ return json .loads (resp .text )['metadata' ]
1071+
10151072 def _get_authority_aliases (self , instance ):
1073+ if self .authority ._is_known_to_developer :
1074+ # Then it is an ADFS/B2C/known_authority_hosts situation
1075+ # which may not reach the central endpoint, so we skip it.
1076+ return []
10161077 if not self .authority_groups :
1017- resp = self .http_client .get (
1018- "https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize" ,
1019- headers = {'Accept' : 'application/json' })
1020- resp .raise_for_status ()
10211078 self .authority_groups = [
1022- set (group ['aliases' ]) for group in json . loads ( resp . text )[ 'metadata' ] ]
1079+ set (group ['aliases' ]) for group in self . _get_instance_metadata () ]
10231080 for group in self .authority_groups :
10241081 if instance in group :
10251082 return [alias for alias in group if alias != instance ]
@@ -1189,7 +1246,8 @@ def acquire_token_silent_with_error(
11891246 the_authority = Authority (
11901247 "https://" + alias + "/" + self .authority .tenant ,
11911248 self .http_client ,
1192- validate_authority = False )
1249+ known_authority_hosts = self ._union_known_authority_hosts (host = alias ),
1250+ )
11931251 result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
11941252 scopes , account , the_authority , force_refresh = force_refresh ,
11951253 claims_challenge = claims_challenge ,
0 commit comments