Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

We have adopted the semantic versioning flow that is industry standard for OSS projects. It gives the maximum amount of control on what risk you take with what versions. If you know how semantic versioning works with node.js, java, and ruby none of this will be new.

## Unreleased behavior changes

For explicit `oidc_authority`, custom authorities no longer accept regional
prefixes on Microsoft issuer hosts or on their own host. Exact built-in
Microsoft authority hosts (case-insensitive) still accept any single nonempty
regional label before a built-in Microsoft issuer host; no region allowlist
is introduced. Microsoft authorities can no longer use the B2C/CIAM issuer
fallback. Exact Microsoft issuer hosts, same-host matching, and existing
B2C/CIAM mappings for non-Microsoft authorities remain supported.

Rejected issuers raise the existing `ValueError` before advertised endpoints
are consumed, and their discovery responses are removed from MSAL's HTTP cache
so later valid metadata can succeed. Ordinary `authority` behavior and existing
URL parsing and scheme handling are unchanged. This is an issuer-policy
behavior change, not a complete credential-theft remediation.

## Semantic Versioning and API stability promises

Microsoft Authentication libraries are independent open source libraries that are used by partners both internal and external to Microsoft. As with the rest of Microsoft, we have moved to a rapid iteration model where bugs are fixed daily and new versions are produced as required. To communicate these frequent changes to external partners and customers, we use semantic versioning for all our public Microsoft Authentication SDK libraries. This follows the practices of other open source libraries on the internet. This allows us to support our downstream partners which will lock on certain versions for stability purposes, as well as providing for the distribution over NuGet, CocoaPods, and Maven.
Expand Down
14 changes: 14 additions & 0 deletions msal/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,20 @@ def get_client_assertion():
MSAL will append ".well-known/openid-configuration" to the authority
and retrieve the OIDC metadata from there, to figure out the endpoints.

Issuer validation raises ``ValueError`` before using the advertised
endpoints when the issuer does not match an accepted pattern.
Exact known Microsoft issuer hosts remain accepted for custom
authorities. Regional issuer hosts are accepted only when the
configured authority host is an exact built-in Microsoft host
(case-insensitive). Any single nonempty regional label is still
accepted for those authorities; there is no region allowlist.
Custom authorities no longer accept regional prefixes on Microsoft
hosts or on their own host. Same-host matching and existing
B2C/CIAM issuer mappings for non-Microsoft authorities are preserved,
but Microsoft authorities cannot use the B2C/CIAM fallback.
Rejected discovery metadata is removed from MSAL's HTTP cache so a
later valid response can succeed.

Note: Broker will NOT be used for OIDC authority.
"""
self.client_id = client_id
Expand Down
47 changes: 28 additions & 19 deletions msal/authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from urlparse import urlparse
import logging

from .throttled_http_client import ThrottledHttpClient

logger = logging.getLogger(__name__)
# Endpoints were copied from here
# https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud#azure-ad-authentication-endpoints
Expand Down Expand Up @@ -113,13 +115,16 @@ def __init__(
) + " Also please double check your tenant name or GUID is correct."
raise ValueError(error_message)
self._issuer = openid_config.get('issuer')
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

# Validate the issuer if using OIDC authority
if self._oidc_authority_url and not self.has_valid_issuer():
is_valid_issuer = not self._oidc_authority_url
try:
if self._oidc_authority_url:
is_valid_issuer = self.has_valid_issuer()
finally:
if not is_valid_issuer and isinstance(self._http_client, ThrottledHttpClient):
self._http_client._forget_get(tenant_discovery_endpoint)
if not is_valid_issuer:
raise ValueError((
"The issuer '{iss}' does not match the authority '{auth}' or a known pattern. "
"When using the 'oidc_authority' parameter in ClientApplication, the authority "
Expand All @@ -128,6 +133,11 @@ def __init__(
"'authority' parameter should be used instead of 'oidc_authority'. "
""
).format(iss=self._issuer, auth=oidc_authority_url))
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
Expand Down Expand Up @@ -208,8 +218,11 @@ def has_valid_issuer(self):
- It exactly matches the authority URL (with/without trailing slash)
- It has the same scheme and host as the authority (path can be different)
- The issuer host is a well-known Microsoft authority host
- The issuer host is a regional variant of a well-known host (e.g., westus2.login.microsoft.com)
- For CIAM, hosts that end with well-known B2C hosts (e.g., tenant.b2clogin.com) are accepted as valid issuers
- For a well-known Microsoft authority only, the issuer host has
exactly one nonempty label before a well-known Microsoft host.
The label is not checked against a list of regions.
- For other authorities, issuer hosts that end with well-known
B2C/CIAM hosts (e.g., tenant.b2clogin.com) are accepted.
"""
if not self._issuer or not self._oidc_authority_url:
return False
Expand All @@ -224,25 +237,20 @@ def has_valid_issuer(self):

if not issuer_host:
return False
authority_host = authority_parsed.hostname.lower() if authority_parsed.hostname else ""
is_microsoft_authority = authority_host in WELL_KNOWN_AUTHORITY_HOSTS

# Case 2: Issuer is from a trusted Microsoft host - O(1) lookup
if issuer_host in WELL_KNOWN_AUTHORITY_HOSTS:
return True

# Case 3: Regional variant check - O(1) lookup
# Case 3: Only built-in Microsoft authorities accept regional issuers.
# e.g., westus2.login.microsoft.com -> extract "login.microsoft.com"
dot_index = issuer_host.find(".")
if dot_index > 0:
if is_microsoft_authority and dot_index > 0:
potential_base = issuer_host[dot_index + 1:]
if "." not in issuer_host[:dot_index]:
# 3a: Base host is a trusted Microsoft host
if potential_base in WELL_KNOWN_AUTHORITY_HOSTS:
return True
# 3b: Issuer has a region prefix on the authority host
# e.g. issuer=us.someweb.com, authority=someweb.com
authority_host = authority_parsed.hostname.lower() if authority_parsed.hostname else ""
if potential_base == authority_host:
return True
if potential_base in WELL_KNOWN_AUTHORITY_HOSTS:
return True

# Case 4: Same scheme and host (path can differ)
if (authority_parsed.scheme == issuer_parsed.scheme and
Expand All @@ -252,7 +260,8 @@ def has_valid_issuer(self):
# Case 5: Check if issuer host is a subdomain of a well-known B2C host
# e.g., tenant.b2clogin.com matches .b2clogin.com
# but fakeb2clogin.com does not
if any(issuer_host.endswith("." + h) for h in WELL_KNOWN_B2C_HOSTS):
if not is_microsoft_authority and any(
issuer_host.endswith("." + h) for h in WELL_KNOWN_B2C_HOSTS):
return True

return False
Expand Down
16 changes: 15 additions & 1 deletion msal/individual_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,21 @@ def __delitem__(self, key): # O(1)
del timestamps[key] # O(1)
self._mapping[self._INDEX] = sequence, timestamps

def _invalidate(self, key):
"""Remove an entry and compact its expiry index under the same lock."""
self._validate_key(key)
with self._lock:
sequence, timestamps = self._peek()
self._mapping.pop(key, None)
timestamps.pop(key, None)
# Rebuild from live entries, including cleanup of previously orphaned records.
sequence[:] = [
[expires_at, created_at, entry_key]
for entry_key, (expires_at, created_at) in timestamps.items()
]
heapq.heapify(sequence)
self._mapping[self._INDEX] = sequence, timestamps

def __len__(self): # O(logN)
"""Drop all expired items and return the remaining length"""
with self._lock:
Expand Down Expand Up @@ -287,4 +302,3 @@ def wrapper(*args, **kwargs):
return value

return wrapper

12 changes: 8 additions & 4 deletions msal/throttled_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,14 @@ def __init__(self, *args, default_throttle_time=None, **kwargs):

self.get = IndividualCache( # Typically those discovery GETs
mapping=self._expiring_mapping,
key_maker=lambda func, args, kwargs: "GET {} hash={} 2xx".format(
args[0], # It is the url, sometimes containing inline params
self._hash(kwargs.get("params", "")),
),
key_maker=lambda func, args, kwargs: self._get_cache_key(
args[0], kwargs.get("params", "")),
expires_in=lambda result=None, **ignored:
3600*24 if 200 <= result.status_code < 300 else 0,
)(self.get)

def _get_cache_key(self, url, params=""):
return "GET {} hash={} 2xx".format(url, self._hash(params))

def _forget_get(self, url):
self._expiring_mapping._invalidate(self._get_cache_key(url))
Loading
Loading