Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion msal/authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ def has_valid_issuer(self):
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):
if any(issuer_host.endswith("." + h) for h in WELL_KNOWN_B2C_HOSTS):
return True
Comment thread
4gust marked this conversation as resolved.
Outdated

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