66
77import msal
88from msal .authority import *
9+ from msal .authority import _CIAM_DOMAIN_SUFFIX # Explicitly import the private constant
910from tests import unittest
1011from tests .http_client import MinimalHttpClient
1112
@@ -100,12 +101,12 @@ def test_authority_with_path_should_be_used_as_is(self, oidc_discovery):
100101
101102
102103@patch ("msal.authority._instance_discovery" )
103- @patch ("msal.authority.tenant_discovery" , return_value = {
104- "authorization_endpoint" : "https://contoso.com/authorize" ,
105- "token_endpoint" : "https://contoso.com/token" ,
106- })
104+ @patch ("msal.authority.tenant_discovery" ) # Moved return_value out of the decorator
107105class OidcAuthorityTestCase (unittest .TestCase ):
108106 authority = "https://contoso.com/tenant"
107+ authorization_endpoint = "https://contoso.com/authorize"
108+ token_endpoint = "https://contoso.com/token"
109+ issuer = "https://contoso.com/tenant" # Added as class variable for inheritance
109110
110111 def setUp (self ):
111112 # setUp() gives subclass a dynamic setup based on their authority
@@ -115,25 +116,37 @@ def setUp(self):
115116 # Here the test is to confirm the OIDC endpoint contains no "/v2.0"
116117 self .authority + "/.well-known/openid-configuration" )
117118
119+ def setup_tenant_discovery (self , tenant_discovery ):
120+ """Configure the tenant_discovery mock with class-specific values"""
121+ tenant_discovery .return_value = {
122+ "authorization_endpoint" : self .authorization_endpoint ,
123+ "token_endpoint" : self .token_endpoint ,
124+ "issuer" : self .issuer ,
125+ }
126+
118127 def test_authority_obj_should_do_oidc_discovery_and_skip_instance_discovery (
119128 self , oidc_discovery , instance_discovery ):
129+ self .setup_tenant_discovery (oidc_discovery )
130+
120131 c = MinimalHttpClient ()
121132 a = Authority (None , c , oidc_authority_url = self .authority )
122133 instance_discovery .assert_not_called ()
123134 oidc_discovery .assert_called_once_with (self .oidc_discovery_endpoint , c )
124- self .assertEqual (a .authorization_endpoint , 'https://contoso.com/authorize' )
125- self .assertEqual (a .token_endpoint , 'https://contoso.com/token' )
135+ self .assertEqual (a .authorization_endpoint , self . authorization_endpoint )
136+ self .assertEqual (a .token_endpoint , self . token_endpoint )
126137
127138 def test_application_obj_should_do_oidc_discovery_and_skip_instance_discovery (
128139 self , oidc_discovery , instance_discovery ):
140+ self .setup_tenant_discovery (oidc_discovery )
141+
129142 app = msal .ClientApplication (
130143 "id" , authority = None , oidc_authority = self .authority )
131144 instance_discovery .assert_not_called ()
132145 oidc_discovery .assert_called_once_with (
133146 self .oidc_discovery_endpoint , app .http_client )
134147 self .assertEqual (
135- app .authority .authorization_endpoint , 'https://contoso.com/authorize' )
136- self .assertEqual (app .authority .token_endpoint , 'https://contoso.com/token' )
148+ app .authority .authorization_endpoint , self . authorization_endpoint )
149+ self .assertEqual (app .authority .token_endpoint , self . token_endpoint )
137150
138151
139152class DstsAuthorityTestCase (OidcAuthorityTestCase ):
@@ -144,14 +157,14 @@ class DstsAuthorityTestCase(OidcAuthorityTestCase):
144157 "https://some.url.dsts.core.azure-test.net/dstsv2/common/oauth2/authorize" )
145158 token_endpoint = (
146159 "https://some.url.dsts.core.azure-test.net/dstsv2/common/oauth2/token" )
160+ issuer = "https://test-instance1-dsts.dsts.core.azure-test.net/dstsv2/common"
147161
148162 @patch ("msal.authority._instance_discovery" )
149- @patch ("msal.authority.tenant_discovery" , return_value = {
150- "authorization_endpoint" : authorization_endpoint ,
151- "token_endpoint" : token_endpoint ,
152- }) # We need to create new patches (i.e. mocks) for non-inherited test cases
163+ @patch ("msal.authority.tenant_discovery" ) # Remove the hard-coded return_value
153164 def test_application_obj_should_accept_dsts_url_as_an_authority (
154165 self , oidc_discovery , instance_discovery ):
166+ self .setup_tenant_discovery (oidc_discovery )
167+
155168 app = msal .ClientApplication ("id" , authority = self .authority )
156169 instance_discovery .assert_not_called ()
157170 oidc_discovery .assert_called_once_with (
@@ -274,3 +287,93 @@ def _test_turning_off_instance_discovery_should_skip_authority_validation_and_in
274287 app .get_accounts () # This could make an instance metadata call for authority aliases
275288 instance_metadata .assert_not_called ()
276289
290+
291+ class TestAuthorityIssuerValidation (unittest .TestCase ):
292+ """Test cases for authority.has_valid_issuer method """
293+
294+ def setUp (self ):
295+ self .http_client = MinimalHttpClient ()
296+
297+ def _create_authority_with_issuer (self , oidc_authority_url , issuer , tenant_discovery_mock ):
298+ tenant_discovery_mock .return_value = {
299+ "authorization_endpoint" : "https://example.com/oauth2/authorize" ,
300+ "token_endpoint" : "https://example.com/oauth2/token" ,
301+ "issuer" : issuer ,
302+ }
303+ authority = Authority (
304+ None ,
305+ self .http_client ,
306+ oidc_authority_url = oidc_authority_url
307+ )
308+ return authority
309+
310+ @patch ("msal.authority.tenant_discovery" )
311+ def test_exact_match_issuer (self , tenant_discovery_mock ):
312+ """Test when issuer exactly matches the OIDC authority URL"""
313+ authority_url = "https://example.com/tenant"
314+ authority = self ._create_authority_with_issuer (authority_url , authority_url , tenant_discovery_mock )
315+ self .assertTrue (authority .has_valid_issuer (), "Issuer should be valid when it exactly matches the authority URL" )
316+
317+ @patch ("msal.authority.tenant_discovery" )
318+ def test_no_issuer (self , tenant_discovery_mock ):
319+ """Test when no issuer is returned from OIDC discovery"""
320+ authority_url = "https://example.com/tenant"
321+ tenant_discovery_mock .return_value = {
322+ "authorization_endpoint" : "https://example.com/oauth2/authorize" ,
323+ "token_endpoint" : "https://example.com/oauth2/token" ,
324+ # No issuer key
325+ }
326+ # Since initialization now checks for valid issuer, we expect it to raise ValueError
327+ with self .assertRaises (ValueError ) as context :
328+ Authority (None , self .http_client , oidc_authority_url = authority_url )
329+ self .assertIn ("issuer" , str (context .exception ).lower ())
330+
331+ @patch ("msal.authority.tenant_discovery" )
332+ def test_microsoft_host_issuer (self , tenant_discovery_mock ):
333+ """Test when issuer has a known Microsoft host"""
334+ authority_url = "https://custom-domain.com/tenant"
335+ issuer = f"https://{ WORLD_WIDE } /tenant"
336+ authority = self ._create_authority_with_issuer (authority_url , issuer , tenant_discovery_mock )
337+ self .assertTrue (authority .has_valid_issuer (), "Issuer should be valid when it has a known Microsoft host" )
338+
339+ @patch ("msal.authority.tenant_discovery" )
340+ def test_same_scheme_and_host_different_path (self , tenant_discovery_mock ):
341+ """Test when issuer has same scheme and host but different path"""
342+ authority_url = "https://example.com/tenant"
343+ issuer = "https://example.com/different/path"
344+ authority = self ._create_authority_with_issuer (authority_url , issuer , tenant_discovery_mock )
345+ self .assertTrue (authority .has_valid_issuer (), "Issuer should be valid when it has the same scheme and host" )
346+
347+ @patch ("msal.authority.tenant_discovery" )
348+ def test_ciam_authority_with_matching_tenant (self , tenant_discovery_mock ):
349+ """Test CIAM authority with matching tenant in path"""
350+ authority_url = "https://custom-domain.com/tenant_name"
351+ issuer = f"https://tenant_name{ _CIAM_DOMAIN_SUFFIX } "
352+ authority = self ._create_authority_with_issuer (authority_url , issuer , tenant_discovery_mock )
353+ self .assertTrue (authority .has_valid_issuer (), "Issuer should be valid for CIAM pattern with matching tenant" )
354+
355+ @patch ("msal.authority.tenant_discovery" )
356+ def test_ciam_authority_with_host_tenant (self , tenant_discovery_mock ):
357+ """Test CIAM authority with tenant in hostname"""
358+ tenant_name = "tenant_name"
359+ authority_url = f"https://{ tenant_name } { _CIAM_DOMAIN_SUFFIX } /custom/path"
360+ issuer = f"https://{ tenant_name } { _CIAM_DOMAIN_SUFFIX } "
361+ authority = self ._create_authority_with_issuer (authority_url , issuer , tenant_discovery_mock )
362+ self .assertTrue (authority .has_valid_issuer (), "Issuer should be valid for CIAM pattern with tenant in hostname" )
363+
364+ @patch ("msal.authority.tenant_discovery" )
365+ def test_invalid_issuer (self , tenant_discovery_mock ):
366+ """Test when issuer is completely different from authority"""
367+ authority_url = "https://example.com/tenant"
368+ issuer = "https://malicious-site.com/tenant"
369+ tenant_discovery_mock .return_value = {
370+ "authorization_endpoint" : "https://example.com/oauth2/authorize" ,
371+ "token_endpoint" : "https://example.com/oauth2/token" ,
372+ "issuer" : issuer ,
373+ }
374+ # Since initialization now checks for valid issuer, we expect it to raise ValueError
375+ with self .assertRaises (ValueError ) as context :
376+ Authority (None , self .http_client , oidc_authority_url = authority_url )
377+ self .assertIn ("issuer" , str (context .exception ).lower ())
378+ self .assertIn (issuer , str (context .exception ))
379+ self .assertIn (authority_url , str (context .exception ))
0 commit comments