Skip to content
Merged
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
6 changes: 4 additions & 2 deletions msal/authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,10 @@ def has_valid_issuer(self):
authority_parsed.netloc == issuer_parsed.netloc):
return True

# Case 5: Check if issuer host ends with any well-known B2C host (e.g., tenant.b2clogin.com)
if any(issuer_host.endswith(h) for h in WELL_KNOWN_B2C_HOSTS):
# 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):
return True

return False
Expand Down
62 changes: 62 additions & 0 deletions tests/test_authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,3 +701,65 @@ def test_ciam_issuer_host_via_b2c_check(self, tenant_discovery_mock):
self.assertTrue(authority.has_valid_issuer(),
"Issuer ending with ciamlogin.com should be valid")

# Domain spoofing prevention tests
@patch("msal.authority.tenant_discovery")
def test_spoofed_b2c_host_should_be_rejected(self, tenant_discovery_mock):
"""fakeb2clogin.com must NOT match b2clogin.com"""
authority_url = "https://custom-domain.com/tenant"
issuer = "https://fakeb2clogin.com/tenant"
tenant_discovery_mock.return_value = {
"authorization_endpoint": "https://example.com/oauth2/authorize",
"token_endpoint": "https://example.com/oauth2/token",
"issuer": issuer,
}
with self.assertRaises(ValueError):
Authority(None, self.http_client, oidc_authority_url=authority_url)

@patch("msal.authority.tenant_discovery")
def test_spoofed_b2c_host_with_prefix_should_be_rejected(self, tenant_discovery_mock):
"""evilb2clogin.com must NOT match b2clogin.com"""
authority_url = "https://custom-domain.com/tenant"
issuer = "https://evilb2clogin.com/tenant"
tenant_discovery_mock.return_value = {
"authorization_endpoint": "https://example.com/oauth2/authorize",
"token_endpoint": "https://example.com/oauth2/token",
"issuer": issuer,
}
with self.assertRaises(ValueError):
Authority(None, self.http_client, oidc_authority_url=authority_url)

@patch("msal.authority.tenant_discovery")
def test_b2c_domain_used_as_subdomain_of_evil_site_should_be_rejected(self, tenant_discovery_mock):
"""b2clogin.com.evil.com must NOT match b2clogin.com"""
authority_url = "https://custom-domain.com/tenant"
issuer = "https://b2clogin.com.evil.com/tenant"
tenant_discovery_mock.return_value = {
"authorization_endpoint": "https://example.com/oauth2/authorize",
"token_endpoint": "https://example.com/oauth2/token",
"issuer": issuer,
}
with self.assertRaises(ValueError):
Authority(None, self.http_client, oidc_authority_url=authority_url)

@patch("msal.authority.tenant_discovery")
def test_spoofed_ciamlogin_host_should_be_rejected(self, tenant_discovery_mock):
"""fakeciamlogin.com must NOT match ciamlogin.com"""
authority_url = "https://custom-domain.com/tenant"
issuer = "https://fakeciamlogin.com/tenant"
tenant_discovery_mock.return_value = {
"authorization_endpoint": "https://example.com/oauth2/authorize",
"token_endpoint": "https://example.com/oauth2/token",
"issuer": issuer,
}
with self.assertRaises(ValueError):
Authority(None, self.http_client, oidc_authority_url=authority_url)

@patch("msal.authority.tenant_discovery")
def test_valid_b2c_subdomain_should_be_accepted(self, tenant_discovery_mock):
"""login.b2clogin.com should match .b2clogin.com"""
authority_url = "https://custom-domain.com/tenant"
issuer = "https://login.b2clogin.com/tenant"
authority = self._create_authority_with_issuer(authority_url, issuer, tenant_discovery_mock)
self.assertTrue(authority.has_valid_issuer(),
"Legitimate subdomain of b2clogin.com should be valid")

Loading