@@ -442,3 +442,121 @@ def test_non_fmi_tokens_not_affected_by_fmi_cache(self):
442442 self .assertEqual (1 , len (at_entries ))
443443 self .assertEqual ("regular_token" , at_entries [0 ]["secret" ])
444444
445+
446+ class TestCrossMsalCacheKeyCompatibility (unittest .TestCase ):
447+ """Verify that _compute_ext_cache_key produces hashes identical to MSAL Go
448+ (CacheExtKeyGenerator) and MSAL .NET (CoreHelpers.ComputeAccessTokenExtCacheKey).
449+
450+ All three libraries use the same algorithm:
451+ 1. Sort key-value pairs alphabetically by key (ordinal / case-sensitive)
452+ 2. Concatenate them: "key1value1key2value2…"
453+ 3. SHA-256 hash
454+ 4. Base64url encode (no padding), lowercased
455+
456+ The expected hashes below are copied from:
457+ - MSAL Go: authority_ext_cachekey_test.go (TestAppKeyWithCacheKeyComponent)
458+ - MSAL .NET: CacheKeyExtensionTests.cs (RunHappyPathTest, CacheExtEnsurePopKeysFunctionAsync)
459+ """
460+
461+ def test_two_params_hash_matches_go_and_dotnet (self ):
462+ """Go/dotnet expected: bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi"""
463+ result = _compute_ext_cache_key ({"key1" : "value1" , "key2" : "value2" })
464+ self .assertEqual ("bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi" , result )
465+
466+ def test_two_different_params_hash_matches_go_and_dotnet (self ):
467+ """Go/dotnet expected: 3-rg6_wyjx5bcy0c3cqq7gajtzgsqy3oxqpwj4y8k4u"""
468+ result = _compute_ext_cache_key ({"key3" : "value3" , "key4" : "value4" })
469+ self .assertEqual ("3-rg6_wyjx5bcy0c3cqq7gajtzgsqy3oxqpwj4y8k4u" , result )
470+
471+ def test_five_params_hash_matches_go_and_dotnet (self ):
472+ """Go/dotnet expected (full hash): rn_gkpxxkkqjxcqnvnmr2duvxg66xanvkz6qfqpwp2e
473+ Go test uses substring match 'gkpxxkkqjxcqnvnmr2duvxg66xanvkz6qfqpwp2e'."""
474+ result = _compute_ext_cache_key ({
475+ "key3" : "value3" , "key4" : "value4" ,
476+ "key5" : "value5" , "key6" : "value6" , "key7" : "value7" ,
477+ })
478+ self .assertEqual ("rn_gkpxxkkqjxcqnvnmr2duvxg66xanvkz6qfqpwp2e" , result )
479+
480+ def test_order_independence_matches_go_and_dotnet (self ):
481+ """Same keys in different insertion order must produce the same hash
482+ (mirrors TestCacheKeyComponentHashConsistency in Go)."""
483+ h1 = _compute_ext_cache_key ({"key3" : "value3" , "key4" : "value4" ,
484+ "key5" : "value5" , "key6" : "value6" , "key7" : "value7" })
485+ h2 = _compute_ext_cache_key ({"key7" : "value7" , "key4" : "value4" ,
486+ "key6" : "value6" , "key5" : "value5" , "key3" : "value3" })
487+ self .assertEqual (h1 , h2 )
488+
489+ def test_at_cache_key_uses_atext_credential_type (self ):
490+ """When ext_cache_key is present the credential type segment of the
491+ AT cache key must be 'atext' (not 'accesstoken'), matching Go/dotnet.
492+
493+ Go: {hid}-{env}-atext-{clientID}-{realm}-{scopes}-{hash}
494+ .NET: {hid}-{env}-atext-{clientID}-{tenantId}-{scopes}-{hash}
495+ """
496+ cache = TokenCache ()
497+ key_maker = cache .key_makers [TokenCache .CredentialType .ACCESS_TOKEN ]
498+ key = key_maker (
499+ home_account_id = "hid" , environment = "env" , client_id = "cid" ,
500+ realm = "realm" , target = "scope" ,
501+ ext_cache_key = "bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi" )
502+ self .assertEqual (
503+ "hid-env-atext-cid-realm-scope-bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi" ,
504+ key )
505+
506+ def test_at_cache_key_without_ext_uses_accesstoken (self ):
507+ """Regular ATs (no ext_cache_key) must keep 'accesstoken' credential type."""
508+ cache = TokenCache ()
509+ key_maker = cache .key_makers [TokenCache .CredentialType .ACCESS_TOKEN ]
510+ key = key_maker (
511+ home_account_id = "hid" , environment = "env" , client_id = "cid" ,
512+ realm = "realm" , target = "scope" )
513+ self .assertEqual ("hid-env-accesstoken-cid-realm-scope" , key )
514+
515+ def test_dotnet_style_full_at_cache_key (self ):
516+ """Reproduce the exact cache key from MSAL .NET CacheKeyExtensionTests:
517+ expectedCacheKey1 = '-login.windows.net-atext-d3adb33f-c0de-ed0c-c0de-deadb33fc0d3-common-r1/scope1 r1/scope2-bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi'
518+ """
519+ cache = TokenCache ()
520+ key_maker = cache .key_makers [TokenCache .CredentialType .ACCESS_TOKEN ]
521+ ext_hash = _compute_ext_cache_key ({"key1" : "value1" , "key2" : "value2" })
522+ key = key_maker (
523+ home_account_id = "" ,
524+ environment = "login.windows.net" ,
525+ client_id = "d3adb33f-c0de-ed0c-c0de-deadb33fc0d3" ,
526+ realm = "common" ,
527+ target = "r1/scope1 r1/scope2" ,
528+ ext_cache_key = ext_hash )
529+ expected = "-login.windows.net-atext-d3adb33f-c0de-ed0c-c0de-deadb33fc0d3-common-r1/scope1 r1/scope2-bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi"
530+ self .assertEqual (expected , key )
531+
532+ def test_dotnet_style_second_cache_key (self ):
533+ """Reproduce CacheKeyExtensionTests expectedCacheKey2."""
534+ cache = TokenCache ()
535+ key_maker = cache .key_makers [TokenCache .CredentialType .ACCESS_TOKEN ]
536+ ext_hash = _compute_ext_cache_key ({"key3" : "value3" , "key4" : "value4" })
537+ key = key_maker (
538+ home_account_id = "" ,
539+ environment = "login.windows.net" ,
540+ client_id = "d3adb33f-c0de-ed0c-c0de-deadb33fc0d3" ,
541+ realm = "common" ,
542+ target = "r1/scope1 r1/scope2" ,
543+ ext_cache_key = ext_hash )
544+ expected = "-login.windows.net-atext-d3adb33f-c0de-ed0c-c0de-deadb33fc0d3-common-r1/scope1 r1/scope2-3-rg6_wyjx5bcy0c3cqq7gajtzgsqy3oxqpwj4y8k4u"
545+ self .assertEqual (expected , key )
546+
547+ def test_go_style_at_cache_key (self ):
548+ """Reproduce the Go AccessToken.Key() format:
549+ Go test: 'testhid-env-atext-clientid-realm-user.read-{hash}'
550+ """
551+ cache = TokenCache ()
552+ key_maker = cache .key_makers [TokenCache .CredentialType .ACCESS_TOKEN ]
553+ ext_hash = _compute_ext_cache_key ({"key1" : "value1" , "key2" : "value2" })
554+ key = key_maker (
555+ home_account_id = "testhid" ,
556+ environment = "env" ,
557+ client_id = "clientid" ,
558+ realm = "realm" ,
559+ target = "user.read" ,
560+ ext_cache_key = ext_hash )
561+ expected = "testhid-env-atext-clientid-realm-user.read-bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi"
562+ self .assertEqual (expected , key )
0 commit comments