55 from urlparse import urlparse
66import logging
77
8- from .exceptions import MsalServiceError
9-
108
119logger = logging .getLogger (__name__ )
1210
2826 "b2clogin.cn" ,
2927 "b2clogin.us" ,
3028 "b2clogin.de" ,
29+ "ciamlogin.com" ,
3130 ]
31+ _CIAM_DOMAIN_SUFFIX = ".ciamlogin.com"
3232
3333
3434class AuthorityBuilder (object ):
@@ -52,12 +52,6 @@ class Authority(object):
5252 """
5353 _domains_without_user_realm_discovery = set ([])
5454
55- @property
56- def http_client (self ): # Obsolete. We will remove this eventually
57- warnings .warn (
58- "authority.http_client might be removed in MSAL Python 1.21+" , DeprecationWarning )
59- return self ._http_client
60-
6155 def __init__ (
6256 self , authority_url , http_client ,
6357 validate_authority = True ,
@@ -80,7 +74,8 @@ def __init__(
8074 if isinstance (authority_url , AuthorityBuilder ):
8175 authority_url = str (authority_url )
8276 authority , self .instance , tenant = canonicalize (authority_url )
83- self .is_adfs = tenant .lower () == 'adfs'
77+ is_ciam = self .instance .endswith (_CIAM_DOMAIN_SUFFIX )
78+ self .is_adfs = tenant .lower () == 'adfs' and not is_ciam
8479 parts = authority .path .split ('/' )
8580 self ._is_b2c = any (
8681 self .instance .endswith ("." + d ) for d in WELL_KNOWN_B2C_HOSTS
@@ -109,13 +104,13 @@ def __init__(
109104 % authority_url )
110105 tenant_discovery_endpoint = payload ['tenant_discovery_endpoint' ]
111106 else :
112- tenant_discovery_endpoint = (
113- 'https://{}:{}{}{} /.well-known/openid-configuration' .format (
114- self . instance ,
115- 443 if authority . port is None else authority .port ,
116- authority . path , # In B2C scenario, it is "/tenant/policy"
117- "" if tenant == "adfs" else "/v2.0" # the AAD v2 endpoint
118- ))
107+ tenant_discovery_endpoint = authority . _replace (
108+ path = "{prefix}{version} /.well-known/openid-configuration" .format (
109+ prefix = tenant if is_ciam and len ( authority . path ) <= 1 # Path-less CIAM
110+ else authority .path , # In B2C, it is "/tenant/policy"
111+ version = "" if self . is_adfs else "/v2.0" ,
112+ )
113+ ). geturl () # Keeping original port and query. Query is useful for test.
119114 try :
120115 openid_config = tenant_discovery (
121116 tenant_discovery_endpoint ,
@@ -150,18 +145,28 @@ def user_realm_discovery(self, username, correlation_id=None, response=None):
150145 return {} # This can guide the caller to fall back normal ROPC flow
151146
152147
153- def canonicalize (authority_url ):
148+ def canonicalize (authority_or_auth_endpoint ):
154149 # Returns (url_parsed_result, hostname_in_lowercase, tenant)
155- authority = urlparse (authority_url )
156- parts = authority .path .split ("/" )
157- if authority .scheme != "https" or len (parts ) < 2 or not parts [1 ]:
158- raise ValueError (
159- "Your given address (%s) should consist of "
160- "an https url with a minimum of one segment in a path: e.g. "
161- "https://login.microsoftonline.com/<tenant> "
162- "or https://<tenant_name>.b2clogin.com/<tenant_name>.onmicrosoft.com/policy"
163- % authority_url )
164- return authority , authority .hostname , parts [1 ]
150+ authority = urlparse (authority_or_auth_endpoint )
151+ if authority .scheme == "https" :
152+ parts = authority .path .split ("/" )
153+ first_part = parts [1 ] if len (parts ) >= 2 and parts [1 ] else None
154+ if authority .hostname .endswith (_CIAM_DOMAIN_SUFFIX ): # CIAM
155+ # Use path in CIAM authority. It will be validated by OIDC Discovery soon
156+ tenant = first_part if first_part else "{}.onmicrosoft.com" .format (
157+ # Fallback to sub domain name. This variation may not be advertised
158+ authority .hostname .rsplit (_CIAM_DOMAIN_SUFFIX , 1 )[0 ])
159+ return authority , authority .hostname , tenant
160+ # AAD
161+ if len (parts ) >= 2 and parts [1 ]:
162+ return authority , authority .hostname , parts [1 ]
163+ raise ValueError (
164+ "Your given address (%s) should consist of "
165+ "an https url with a minimum of one segment in a path: e.g. "
166+ "https://login.microsoftonline.com/<tenant> "
167+ "or https://<tenant_name>.ciamlogin.com/<tenant> "
168+ "or https://<tenant_name>.b2clogin.com/<tenant_name>.onmicrosoft.com/policy"
169+ % authority_or_auth_endpoint )
165170
166171def _instance_discovery (url , http_client , instance_discovery_endpoint , ** kwargs ):
167172 resp = http_client .get (
@@ -174,16 +179,14 @@ def tenant_discovery(tenant_discovery_endpoint, http_client, **kwargs):
174179 # Returns Openid Configuration
175180 resp = http_client .get (tenant_discovery_endpoint , ** kwargs )
176181 if resp .status_code == 200 :
177- payload = json .loads (resp .text ) # It could raise ValueError
178- if 'authorization_endpoint' in payload and 'token_endpoint' in payload :
179- return payload # Happy path
180- raise ValueError ("OIDC Discovery does not provide enough information" )
182+ return json .loads (resp .text ) # It could raise ValueError
181183 if 400 <= resp .status_code < 500 :
182184 # Nonexist tenant would hit this path
183185 # e.g. https://login.microsoftonline.com/nonexist_tenant/v2.0/.well-known/openid-configuration
184- raise ValueError (
185- "OIDC Discovery endpoint rejects our request. Error: {}" .format (
186- resp .text # Expose it as-is b/c OIDC defines no error response format
186+ raise ValueError ("OIDC Discovery failed on {}. HTTP status: {}, Error: {}" .format (
187+ tenant_discovery_endpoint ,
188+ resp .status_code ,
189+ resp .text , # Expose it as-is b/c OIDC defines no error response format
187190 ))
188191 # Transient network error would hit this path
189192 resp .raise_for_status ()
0 commit comments