From c04af8d4fe183d08dc23402248098950d99c1886 Mon Sep 17 00:00:00 2001 From: Nilesh Choudhary Date: Tue, 7 Apr 2026 09:00:09 +0100 Subject: [PATCH 1/2] added a additional check for domain checking in b2c --- msal/authority.py | 2 +- tests/test_authority.py | 62 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/msal/authority.py b/msal/authority.py index 1b6fefa7..78efb094 100644 --- a/msal/authority.py +++ b/msal/authority.py @@ -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 return False diff --git a/tests/test_authority.py b/tests/test_authority.py index d19a8b4b..2f9d1605 100644 --- a/tests/test_authority.py +++ b/tests/test_authority.py @@ -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") + From 21006ef5173dcb01749a6fd5acb2bd88cfa8b7ab Mon Sep 17 00:00:00 2001 From: Nilesh Choudhary Date: Tue, 7 Apr 2026 09:15:27 +0100 Subject: [PATCH 2/2] updated comment --- msal/authority.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/msal/authority.py b/msal/authority.py index 78efb094..f902e10c 100644 --- a/msal/authority.py +++ b/msal/authority.py @@ -250,7 +250,9 @@ 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) + # 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