Skip to content

Commit 70e09fb

Browse files
committed
Implements a new optional oidc_authority parameter
1 parent c442c78 commit 70e09fb

File tree

3 files changed

+92
-20
lines changed

3 files changed

+92
-20
lines changed

msal/application.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ def __init__(
204204
instance_discovery=None,
205205
allow_broker=None,
206206
enable_pii_log=None,
207+
oidc_authority=None,
207208
):
208209
"""Create an instance of application.
209210
@@ -455,6 +456,15 @@ def __init__(
455456
The default behavior is False.
456457
457458
New in version 1.24.0.
459+
460+
:param str oidc_authority:
461+
*Added in version 1.28.0*:
462+
It is a URL that identifies an OpenID Connect (OIDC) authority of
463+
the format ``https://contoso.com/tenant``.
464+
MSAL will append ".well-known/openid-configuration" to the authority
465+
and retrieve the OIDC metadata from there, to figure out the endpoints.
466+
467+
Note: Broker will NOT be used for OIDC authority.
458468
"""
459469
self.client_id = client_id
460470
self.client_credential = client_credential
@@ -499,18 +509,21 @@ def __init__(
499509
self.app_version = app_version
500510

501511
# Here the self.authority will not be the same type as authority in input
512+
if oidc_authority and authority:
513+
raise ValueError("You can not provide both authority and oidc_authority")
502514
try:
503515
authority_to_use = authority or "https://{}/common/".format(WORLD_WIDE)
504516
self.authority = Authority(
505517
authority_to_use,
506518
self.http_client,
507519
validate_authority=validate_authority,
508520
instance_discovery=self._instance_discovery,
521+
oidc_authority_url=oidc_authority,
509522
)
510523
except ValueError: # Those are explicit authority validation errors
511524
raise
512525
except Exception: # The rest are typically connection errors
513-
if validate_authority and azure_region:
526+
if validate_authority and azure_region and not oidc_authority:
514527
# Since caller opts in to use region, here we tolerate connection
515528
# errors happened during authority validation at non-region endpoint
516529
self.authority = Authority(

msal/authority.py

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def __init__(
5656
self, authority_url, http_client,
5757
validate_authority=True,
5858
instance_discovery=None,
59+
oidc_authority_url=None,
5960
):
6061
"""Creates an authority instance, and also validates it.
6162
@@ -65,12 +66,56 @@ def __init__(
6566
This parameter only controls whether an instance discovery will be
6667
performed.
6768
"""
69+
self._http_client = http_client
70+
if oidc_authority_url:
71+
logger.info("Initializing with OIDC authority: %s", oidc_authority_url)
72+
tenant_discovery_endpoint = self._initialize_oidc_authority(
73+
oidc_authority_url)
74+
else:
75+
logger.info("Initializing with Entra authority: %s", authority_url)
76+
tenant_discovery_endpoint = self._initialize_entra_authority(
77+
authority_url, validate_authority, instance_discovery)
78+
try:
79+
openid_config = tenant_discovery(
80+
tenant_discovery_endpoint,
81+
self._http_client)
82+
except ValueError:
83+
error_message = (
84+
"Unable to get OIDC authority configuration for {url} "
85+
"because its OIDC Discovery endpoint is unavailable at "
86+
"{url}/.well-known/openid-configuration ".format(url=oidc_authority_url)
87+
if oidc_authority_url else
88+
"Unable to get authority configuration for {}. "
89+
"Authority would typically be in a format of "
90+
"https://login.microsoftonline.com/your_tenant "
91+
"or https://tenant_name.ciamlogin.com "
92+
"or https://tenant_name.b2clogin.com/tenant.onmicrosoft.com/policy. "
93+
.format(authority_url)
94+
) + " Also please double check your tenant name or GUID is correct."
95+
raise ValueError(error_message)
96+
logger.debug(
97+
'openid_config("%s") = %s', tenant_discovery_endpoint, openid_config)
98+
self.authorization_endpoint = openid_config['authorization_endpoint']
99+
self.token_endpoint = openid_config['token_endpoint']
100+
self.device_authorization_endpoint = openid_config.get('device_authorization_endpoint')
101+
_, _, self.tenant = canonicalize(self.token_endpoint) # Usually a GUID
102+
103+
def _initialize_oidc_authority(self, oidc_authority_url):
104+
authority, self.instance, tenant = canonicalize(oidc_authority_url)
105+
self.is_adfs = tenant.lower() == 'adfs' # As a convention
106+
self._is_b2c = True # Not exactly true, but
107+
# OIDC Authority was designed for CIAM which is the next gen of B2C.
108+
# Besides, application.py uses this to bypass broker.
109+
self._is_known_to_developer = True # Not really relevant, but application.py uses this to bypass authority validation
110+
return oidc_authority_url + "/.well-known/openid-configuration"
111+
112+
def _initialize_entra_authority(
113+
self, authority_url, validate_authority, instance_discovery):
68114
# :param instance_discovery:
69115
# By default, the known-to-Microsoft validation will use an
70116
# instance discovery endpoint located at ``login.microsoftonline.com``.
71117
# You can customize the endpoint by providing a url as a string.
72118
# Or you can turn this behavior off by passing in a False here.
73-
self._http_client = http_client
74119
if isinstance(authority_url, AuthorityBuilder):
75120
authority_url = str(authority_url)
76121
authority, self.instance, tenant = canonicalize(authority_url)
@@ -111,24 +156,7 @@ def __init__(
111156
version="" if self.is_adfs else "/v2.0",
112157
)
113158
).geturl() # Keeping original port and query. Query is useful for test.
114-
try:
115-
openid_config = tenant_discovery(
116-
tenant_discovery_endpoint,
117-
self._http_client)
118-
except ValueError:
119-
raise ValueError(
120-
"Unable to get authority configuration for {}. "
121-
"Authority would typically be in a format of "
122-
"https://login.microsoftonline.com/your_tenant "
123-
"or https://tenant_name.ciamlogin.com "
124-
"or https://tenant_name.b2clogin.com/tenant.onmicrosoft.com/policy. "
125-
"Also please double check your tenant name or GUID is correct.".format(
126-
authority_url))
127-
logger.debug("openid_config = %s", openid_config)
128-
self.authorization_endpoint = openid_config['authorization_endpoint']
129-
self.token_endpoint = openid_config['token_endpoint']
130-
self.device_authorization_endpoint = openid_config.get('device_authorization_endpoint')
131-
_, _, self.tenant = canonicalize(self.token_endpoint) # Usually a GUID
159+
return tenant_discovery_endpoint
132160

133161
def user_realm_discovery(self, username, correlation_id=None, response=None):
134162
# It will typically return a dict containing "ver", "account_type",

tests/test_authority.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,37 @@ def test_authority_with_path_should_be_used_as_is(self, oidc_discovery):
9999
self.http_client)
100100

101101

102+
@patch("msal.authority._instance_discovery")
103+
@patch("msal.authority.tenant_discovery", return_value={
104+
"authorization_endpoint": "https://contoso.com/authorize",
105+
"token_endpoint": "https://contoso.com/token",
106+
})
107+
class TestOidcAuthority(unittest.TestCase):
108+
def test_authority_obj_should_do_oidc_discovery_and_skip_instance_discovery(
109+
self, oidc_discovery, instance_discovery):
110+
c = MinimalHttpClient()
111+
a = Authority(None, c, oidc_authority_url="https://contoso.com/tenant")
112+
instance_discovery.assert_not_called()
113+
oidc_discovery.assert_called_once_with(
114+
"https://contoso.com/tenant/.well-known/openid-configuration", c)
115+
self.assertEqual(a.authorization_endpoint, 'https://contoso.com/authorize')
116+
self.assertEqual(a.token_endpoint, 'https://contoso.com/token')
117+
118+
def test_application_obj_should_do_oidc_discovery_and_skip_instance_discovery(
119+
self, oidc_discovery, instance_discovery):
120+
app = msal.ClientApplication(
121+
"id",
122+
authority=None,
123+
oidc_authority="https://contoso.com/tenant",
124+
)
125+
instance_discovery.assert_not_called()
126+
oidc_discovery.assert_called_once_with(
127+
"https://contoso.com/tenant/.well-known/openid-configuration",
128+
app.http_client)
129+
self.assertEqual(
130+
app.authority.authorization_endpoint, 'https://contoso.com/authorize')
131+
self.assertEqual(app.authority.token_endpoint, 'https://contoso.com/token')
132+
102133
class TestAuthorityInternalHelperCanonicalize(unittest.TestCase):
103134

104135
def test_canonicalize_tenant_followed_by_extra_paths(self):

0 commit comments

Comments
 (0)