Skip to content

Commit

Permalink
Implements a new optional oidc_authority parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
rayluo committed Mar 16, 2024
1 parent c442c78 commit 70e09fb
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 20 deletions.
15 changes: 14 additions & 1 deletion msal/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ def __init__(
instance_discovery=None,
allow_broker=None,
enable_pii_log=None,
oidc_authority=None,
):
"""Create an instance of application.
Expand Down Expand Up @@ -455,6 +456,15 @@ def __init__(
The default behavior is False.
New in version 1.24.0.
:param str oidc_authority:
*Added in version 1.28.0*:
It is a URL that identifies an OpenID Connect (OIDC) authority of
the format ``https://contoso.com/tenant``.
MSAL will append ".well-known/openid-configuration" to the authority
and retrieve the OIDC metadata from there, to figure out the endpoints.
Note: Broker will NOT be used for OIDC authority.
"""
self.client_id = client_id
self.client_credential = client_credential
Expand Down Expand Up @@ -499,18 +509,21 @@ def __init__(
self.app_version = app_version

# Here the self.authority will not be the same type as authority in input
if oidc_authority and authority:
raise ValueError("You can not provide both authority and oidc_authority")
try:
authority_to_use = authority or "https://{}/common/".format(WORLD_WIDE)
self.authority = Authority(
authority_to_use,
self.http_client,
validate_authority=validate_authority,
instance_discovery=self._instance_discovery,
oidc_authority_url=oidc_authority,
)
except ValueError: # Those are explicit authority validation errors
raise
except Exception: # The rest are typically connection errors
if validate_authority and azure_region:
if validate_authority and azure_region and not oidc_authority:
# Since caller opts in to use region, here we tolerate connection
# errors happened during authority validation at non-region endpoint
self.authority = Authority(
Expand Down
66 changes: 47 additions & 19 deletions msal/authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def __init__(
self, authority_url, http_client,
validate_authority=True,
instance_discovery=None,
oidc_authority_url=None,
):
"""Creates an authority instance, and also validates it.
Expand All @@ -65,12 +66,56 @@ def __init__(
This parameter only controls whether an instance discovery will be
performed.
"""
self._http_client = http_client
if oidc_authority_url:
logger.info("Initializing with OIDC authority: %s", oidc_authority_url)
tenant_discovery_endpoint = self._initialize_oidc_authority(
oidc_authority_url)
else:
logger.info("Initializing with Entra authority: %s", authority_url)
tenant_discovery_endpoint = self._initialize_entra_authority(
authority_url, validate_authority, instance_discovery)
try:
openid_config = tenant_discovery(
tenant_discovery_endpoint,
self._http_client)
except ValueError:
error_message = (
"Unable to get OIDC authority configuration for {url} "
"because its OIDC Discovery endpoint is unavailable at "
"{url}/.well-known/openid-configuration ".format(url=oidc_authority_url)
if oidc_authority_url else
"Unable to get authority configuration for {}. "
"Authority would typically be in a format of "
"https://login.microsoftonline.com/your_tenant "
"or https://tenant_name.ciamlogin.com "
"or https://tenant_name.b2clogin.com/tenant.onmicrosoft.com/policy. "
.format(authority_url)
) + " Also please double check your tenant name or GUID is correct."
raise ValueError(error_message)
logger.debug(
'openid_config("%s") = %s', tenant_discovery_endpoint, openid_config)
self.authorization_endpoint = openid_config['authorization_endpoint']
self.token_endpoint = openid_config['token_endpoint']
self.device_authorization_endpoint = openid_config.get('device_authorization_endpoint')
_, _, self.tenant = canonicalize(self.token_endpoint) # Usually a GUID

def _initialize_oidc_authority(self, oidc_authority_url):
authority, self.instance, tenant = canonicalize(oidc_authority_url)
self.is_adfs = tenant.lower() == 'adfs' # As a convention
self._is_b2c = True # Not exactly true, but
# OIDC Authority was designed for CIAM which is the next gen of B2C.
# Besides, application.py uses this to bypass broker.
self._is_known_to_developer = True # Not really relevant, but application.py uses this to bypass authority validation
return oidc_authority_url + "/.well-known/openid-configuration"

def _initialize_entra_authority(
self, authority_url, validate_authority, instance_discovery):
# :param instance_discovery:
# By default, the known-to-Microsoft validation will use an
# instance discovery endpoint located at ``login.microsoftonline.com``.
# You can customize the endpoint by providing a url as a string.
# Or you can turn this behavior off by passing in a False here.
self._http_client = http_client
if isinstance(authority_url, AuthorityBuilder):
authority_url = str(authority_url)
authority, self.instance, tenant = canonicalize(authority_url)
Expand Down Expand Up @@ -111,24 +156,7 @@ def __init__(
version="" if self.is_adfs else "/v2.0",
)
).geturl() # Keeping original port and query. Query is useful for test.
try:
openid_config = tenant_discovery(
tenant_discovery_endpoint,
self._http_client)
except ValueError:
raise ValueError(
"Unable to get authority configuration for {}. "
"Authority would typically be in a format of "
"https://login.microsoftonline.com/your_tenant "
"or https://tenant_name.ciamlogin.com "
"or https://tenant_name.b2clogin.com/tenant.onmicrosoft.com/policy. "
"Also please double check your tenant name or GUID is correct.".format(
authority_url))
logger.debug("openid_config = %s", openid_config)
self.authorization_endpoint = openid_config['authorization_endpoint']
self.token_endpoint = openid_config['token_endpoint']
self.device_authorization_endpoint = openid_config.get('device_authorization_endpoint')
_, _, self.tenant = canonicalize(self.token_endpoint) # Usually a GUID
return tenant_discovery_endpoint

def user_realm_discovery(self, username, correlation_id=None, response=None):
# It will typically return a dict containing "ver", "account_type",
Expand Down
31 changes: 31 additions & 0 deletions tests/test_authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,37 @@ def test_authority_with_path_should_be_used_as_is(self, oidc_discovery):
self.http_client)


@patch("msal.authority._instance_discovery")
@patch("msal.authority.tenant_discovery", return_value={
"authorization_endpoint": "https://contoso.com/authorize",
"token_endpoint": "https://contoso.com/token",
})
class TestOidcAuthority(unittest.TestCase):
def test_authority_obj_should_do_oidc_discovery_and_skip_instance_discovery(
self, oidc_discovery, instance_discovery):
c = MinimalHttpClient()
a = Authority(None, c, oidc_authority_url="https://contoso.com/tenant")
instance_discovery.assert_not_called()
oidc_discovery.assert_called_once_with(
"https://contoso.com/tenant/.well-known/openid-configuration", c)
self.assertEqual(a.authorization_endpoint, 'https://contoso.com/authorize')
self.assertEqual(a.token_endpoint, 'https://contoso.com/token')

def test_application_obj_should_do_oidc_discovery_and_skip_instance_discovery(
self, oidc_discovery, instance_discovery):
app = msal.ClientApplication(
"id",
authority=None,
oidc_authority="https://contoso.com/tenant",
)
instance_discovery.assert_not_called()
oidc_discovery.assert_called_once_with(
"https://contoso.com/tenant/.well-known/openid-configuration",
app.http_client)
self.assertEqual(
app.authority.authorization_endpoint, 'https://contoso.com/authorize')
self.assertEqual(app.authority.token_endpoint, 'https://contoso.com/token')

class TestAuthorityInternalHelperCanonicalize(unittest.TestCase):

def test_canonicalize_tenant_followed_by_extra_paths(self):
Expand Down

0 comments on commit 70e09fb

Please sign in to comment.