@@ -149,5 +149,135 @@ def test_acquire_with_assertion_callback_and_fmi_path(self):
149149 "cache might not be working correctly" )
150150
151151
152+ class TestFMICacheIsolation (LabBasedTestCase ):
153+ """Test that tokens acquired with different FMI paths are cached separately.
154+
155+ This verifies the cache key extensibility: two calls with different fmi_path
156+ values should NOT return each other's cached tokens.
157+ """
158+
159+ def test_different_fmi_paths_are_cached_separately (self ):
160+ app = msal .ConfidentialClientApplication (
161+ _FMI_CLIENT_ID ,
162+ client_credential = get_client_certificate (),
163+ authority = _AUTHORITY_URL ,
164+ http_client = MinimalHttpClient (),
165+ )
166+ scopes = [_FMI_SCOPE ]
167+
168+ # Acquire token with path A
169+ result_a = app .acquire_token_for_client_with_fmi_path (
170+ scopes , "PathA/credential" )
171+ self .assertIn ("access_token" , result_a ,
172+ "Path A acquisition failed: {}: {}" .format (
173+ result_a .get ("error" ), result_a .get ("error_description" )))
174+
175+ # Acquire token with path B — should NOT get path A's cached token
176+ result_b = app .acquire_token_for_client_with_fmi_path (
177+ scopes , "PathB/credential" )
178+ self .assertIn ("access_token" , result_b ,
179+ "Path B acquisition failed: {}: {}" .format (
180+ result_b .get ("error" ), result_b .get ("error_description" )))
181+ self .assertNotEqual (
182+ result_b .get ("token_source" ), "cache" ,
183+ "Different FMI path should NOT return cached token from another path" )
184+
185+ # Verify path A still returns its own cached token
186+ result_a2 = app .acquire_token_for_client_with_fmi_path (
187+ scopes , "PathA/credential" )
188+ self .assertIn ("access_token" , result_a2 )
189+ self .assertEqual (
190+ result_a2 .get ("token_source" ), "cache" ,
191+ "Same FMI path should return cached token" )
192+ self .assertEqual (result_a ["access_token" ], result_a2 ["access_token" ])
193+
194+ def test_fmi_token_does_not_interfere_with_non_fmi_token (self ):
195+ app = msal .ConfidentialClientApplication (
196+ _FMI_CLIENT_ID ,
197+ client_credential = get_client_certificate (),
198+ authority = _AUTHORITY_URL ,
199+ http_client = MinimalHttpClient (),
200+ )
201+ scopes = [_FMI_SCOPE ]
202+
203+ # Cache a token via FMI path
204+ fmi_result = app .acquire_token_for_client_with_fmi_path (scopes , _FMI_PATH )
205+ self .assertIn ("access_token" , fmi_result )
206+
207+ # Regular acquire_token_for_client should NOT get the FMI token
208+ regular_result = app .acquire_token_for_client (scopes )
209+ self .assertIn ("access_token" , regular_result ,
210+ "Regular call failed: {}: {}" .format (
211+ regular_result .get ("error" ), regular_result .get ("error_description" )))
212+ self .assertNotEqual (
213+ regular_result .get ("token_source" ), "cache" ,
214+ "Non-FMI call should not return FMI-cached token" )
215+
216+
217+ class TestFMICacheInspection (LabBasedTestCase ):
218+ """Acquire tokens with two different FMI paths and inspect the underlying
219+ cache to verify the entries are correctly isolated."""
220+
221+ def test_two_fmi_paths_produce_separate_cache_entries (self ):
222+ app = msal .ConfidentialClientApplication (
223+ _FMI_CLIENT_ID ,
224+ client_credential = get_client_certificate (),
225+ authority = _AUTHORITY_URL ,
226+ http_client = MinimalHttpClient (),
227+ )
228+ scopes = [_FMI_SCOPE ]
229+ path_a = "PathAlpha/Credential"
230+ path_b = "PathBeta/Credential"
231+
232+ # 1. Acquire token with path A
233+ result_a = app .acquire_token_for_client_with_fmi_path (scopes , path_a )
234+ self .assertIn ("access_token" , result_a ,
235+ "Path A acquisition failed: {}: {}" .format (
236+ result_a .get ("error" ), result_a .get ("error_description" )))
237+ token_a = result_a ["access_token" ]
238+
239+ # 2. Acquire token with path B
240+ result_b = app .acquire_token_for_client_with_fmi_path (scopes , path_b )
241+ self .assertIn ("access_token" , result_b ,
242+ "Path B acquisition failed: {}: {}" .format (
243+ result_b .get ("error" ), result_b .get ("error_description" )))
244+ token_b = result_b ["access_token" ]
245+
246+ # Tokens should be different (different paths go to different resources)
247+ self .assertNotEqual (token_a , token_b ,
248+ "Tokens for different FMI paths should differ" )
249+
250+ # 3. Inspect cache: there should be exactly 2 AccessToken entries
251+ cache = app .token_cache ._cache
252+ at_entries = cache .get ("AccessToken" , {})
253+ # Filter to our client_id + scope to avoid noise
254+ our_entries = {
255+ k : v for k , v in at_entries .items ()
256+ if v .get ("client_id" ) == _FMI_CLIENT_ID
257+ and _FMI_SCOPE .split ("/" )[0 ] in v .get ("target" , "" )
258+ }
259+ self .assertEqual (2 , len (our_entries ),
260+ "Cache should contain exactly 2 AT entries for our client, "
261+ "got {}: {}" .format (len (our_entries ), list (our_entries .keys ())))
262+
263+ # 4. Each entry must have a non-empty ext_cache_key, and they must differ
264+ ext_keys = [v .get ("ext_cache_key" ) for v in our_entries .values ()]
265+ for ek in ext_keys :
266+ self .assertTrue (ek , "Each FMI cache entry must have a non-empty ext_cache_key" )
267+ self .assertNotEqual (ext_keys [0 ], ext_keys [1 ],
268+ "ext_cache_key values for different FMI paths must differ" )
269+
270+ # 5. Verify each path still returns its own cached token
271+ cached_a = app .acquire_token_for_client_with_fmi_path (scopes , path_a )
272+ self .assertEqual ("cache" , cached_a .get ("token_source" ))
273+ self .assertEqual (token_a , cached_a ["access_token" ],
274+ "Path A should return its own cached token" )
275+
276+ cached_b = app .acquire_token_for_client_with_fmi_path (scopes , path_b )
277+ self .assertEqual ("cache" , cached_b .get ("token_source" ))
278+ self .assertEqual (token_b , cached_b ["access_token" ],
279+ "Path B should return its own cached token" )
280+
281+
152282if __name__ == "__main__" :
153283 unittest .main ()
0 commit comments