@@ -392,29 +392,6 @@ def test_different_client_claims_produce_different_hashes(self):
392392 def test_empty_client_claims_value_is_ignored (self ):
393393 self .assertEqual ("" , _compute_ext_cache_key ({"client_claims" : "" }))
394394
395- def test_length_prefixed_encoding_avoids_boundary_collision (self ):
396- # Mirrors Go's TestCacheKeyComponentHashNoBoundaryCollision. With a plain
397- # key+value concatenation (no separators) these two distinct component
398- # sets both render to "axbYbZ" (sorted keys "a","b") and would collide,
399- # returning the wrong cached token. The length-prefixed encoding must keep
400- # them distinct. This matters because client_claims is arbitrary caller
401- # JSON that can embed another component's key (e.g. "fmi_path").
402- h1 = _compute_ext_cache_key ({"a" : "xbY" , "b" : "Z" })
403- h2 = _compute_ext_cache_key ({"a" : "x" , "b" : "YbZ" })
404- self .assertNotEqual (
405- h1 , h2 ,
406- "distinct cache key components must not produce the same hash" )
407-
408- def test_client_claims_and_fmi_path_do_not_collide_at_boundary (self ):
409- # Realistic surface: client_claims and fmi_path co-occur in
410- # acquire_token_for_client. A claims value that happens to contain the
411- # other component's key+value at a boundary must not collide.
412- h1 = _compute_ext_cache_key (
413- {"client_claims" : "Xfmi_pathY" , "fmi_path" : "Z" })
414- h2 = _compute_ext_cache_key (
415- {"client_claims" : "X" , "fmi_path" : "Yfmi_pathZ" })
416- self .assertNotEqual (h1 , h2 )
417-
418395
419396class TestClaimsHelpers (unittest .TestCase ):
420397 """Tests for the shared _parse_claims_or_raise / _merge_claims helpers."""
@@ -565,48 +542,45 @@ def test_non_fmi_tokens_not_affected_by_fmi_cache(self):
565542
566543
567544class TestCrossMsalCacheKeyCompatibility (unittest .TestCase ):
568- """Verify that _compute_ext_cache_key matches MSAL Go's CacheExtKeyGenerator
569- (post collision-fix, AzureAD/microsoft-authentication-library-for-go#629 ).
545+ """Verify that _compute_ext_cache_key produces hashes identical to MSAL .NET
546+ (CoreHelpers.ComputeAccessTokenExtCacheKey ).
570547
571548 The algorithm:
572549 1. Sort key-value pairs alphabetically by key (ordinal / case-sensitive)
573- 2. Concatenate length-prefixed pairs ("{len(k)}:{k}{len(v)}:{v}" per pair;
574- e.g. {"key1": "value1"} -> "4:key16:value1"). The length prefixes make
575- the encoding injective -- see TestClientClaimsCacheKey for the collision
576- guard.
550+ 2. Concatenate them with no separators: "key1value1key2value2…"
577551 3. SHA-256 hash
578552 4. Base64url encode (no padding), lowercased
579553
580- The expected hashes below are copied from MSAL Go's
581- authority_ext_cachekey_test.go (TestAppKeyWithCacheKeyComponent ).
554+ The expected hashes below are copied from MSAL .NET's CacheKeyExtensionTests.cs
555+ (RunHappyPathTest, CacheExtEnsurePopKeysFunctionAsync ).
582556
583- NOTE: MSAL .NET 's ComputeAccessTokenExtCacheKey still uses an *unprefixed *
584- concatenation , so these hashes are intentionally NOT byte-identical to current
585- .NET. The cache *key format* (the 'atext' segment layout, asserted below) still
586- matches both Go and .NET; only the trailing hash bytes differ. Caches are not
587- shared across languages, so within-process injectivity -- not cross-language
588- byte-parity -- is what matters for correctness.
557+ NOTE: MSAL Go 's CacheExtKeyGenerator has since switched to a *length-prefixed *
558+ encoding (AzureAD/microsoft-authentication-library-for-go#629) , so these hashes
559+ are intentionally NOT byte-identical to current Go; Python deliberately tracks
560+ .NET here. The cache *key format* ( the 'atext' segment layout, asserted below)
561+ still matches both Go and .NET. Caches are not shared across languages, so this
562+ cross-language hash difference does not affect runtime correctness.
589563 """
590564
591- def test_two_params_hash_matches_go (self ):
592- """Go expected: latlwkpewb_a0rcsmjvkecqt0_huumkw4sflzociike """
565+ def test_two_params_hash_matches_dotnet (self ):
566+ """.NET expected: bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi """
593567 result = _compute_ext_cache_key ({"key1" : "value1" , "key2" : "value2" })
594- self .assertEqual ("latlwkpewb_a0rcsmjvkecqt0_huumkw4sflzociike " , result )
568+ self .assertEqual ("bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi " , result )
595569
596- def test_two_different_params_hash_matches_go (self ):
597- """Go expected: jjoe9jgfmdtnj0rzuetsqy7kzs2m1xfnjjxwsfxsrxq """
570+ def test_two_different_params_hash_matches_dotnet (self ):
571+ """.NET expected: 3-rg6_wyjx5bcy0c3cqq7gajtzgsqy3oxqpwj4y8k4u """
598572 result = _compute_ext_cache_key ({"key3" : "value3" , "key4" : "value4" })
599- self .assertEqual ("jjoe9jgfmdtnj0rzuetsqy7kzs2m1xfnjjxwsfxsrxq " , result )
573+ self .assertEqual ("3-rg6_wyjx5bcy0c3cqq7gajtzgsqy3oxqpwj4y8k4u " , result )
600574
601- def test_five_params_hash_matches_go (self ):
602- """Go expected: prrdp31y37ufw3lo7hly0oimjjvg_34m9ji30ocu4tw """
575+ def test_five_params_hash_matches_dotnet (self ):
576+ """.NET expected (full hash): rn_gkpxxkkqjxcqnvnmr2duvxg66xanvkz6qfqpwp2e """
603577 result = _compute_ext_cache_key ({
604578 "key3" : "value3" , "key4" : "value4" ,
605579 "key5" : "value5" , "key6" : "value6" , "key7" : "value7" ,
606580 })
607- self .assertEqual ("prrdp31y37ufw3lo7hly0oimjjvg_34m9ji30ocu4tw " , result )
581+ self .assertEqual ("rn_gkpxxkkqjxcqnvnmr2duvxg66xanvkz6qfqpwp2e " , result )
608582
609- def test_order_independence_matches_go (self ):
583+ def test_order_independence_matches_dotnet (self ):
610584 """Same keys in different insertion order must produce the same hash
611585 (mirrors TestCacheKeyComponentHashConsistency in Go)."""
612586 h1 = _compute_ext_cache_key ({"key3" : "value3" , "key4" : "value4" ,
@@ -627,9 +601,9 @@ def test_at_cache_key_uses_atext_credential_type(self):
627601 key = key_maker (
628602 home_account_id = "hid" , environment = "env" , client_id = "cid" ,
629603 realm = "realm" , target = "scope" ,
630- ext_cache_key = "latlwkpewb_a0rcsmjvkecqt0_huumkw4sflzociike " )
604+ ext_cache_key = "bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi " )
631605 self .assertEqual (
632- "hid-env-atext-cid-realm-scope-latlwkpewb_a0rcsmjvkecqt0_huumkw4sflzociike " ,
606+ "hid-env-atext-cid-realm-scope-bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi " ,
633607 key )
634608
635609 def test_at_cache_key_without_ext_uses_accesstoken (self ):
@@ -641,10 +615,9 @@ def test_at_cache_key_without_ext_uses_accesstoken(self):
641615 realm = "realm" , target = "scope" )
642616 self .assertEqual ("hid-env-accesstoken-cid-realm-scope" , key )
643617
644- def test_atext_full_at_cache_key_format (self ):
645- """The AT cache key *format* matches MSAL .NET's CacheKeyExtensionTests
646- layout ('-{env}-atext-{clientId}-{tenant}-{scopes}-{hash}'); only the
647- trailing hash now follows Go's length-prefixed encoding (see class note).
618+ def test_dotnet_style_full_at_cache_key (self ):
619+ """Reproduce the exact cache key from MSAL .NET CacheKeyExtensionTests:
620+ expectedCacheKey1 = '-login.windows.net-atext-d3adb33f-c0de-ed0c-c0de-deadb33fc0d3-common-r1/scope1 r1/scope2-bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi'
648621 """
649622 cache = TokenCache ()
650623 key_maker = cache .key_makers [TokenCache .CredentialType .ACCESS_TOKEN ]
@@ -656,11 +629,11 @@ def test_atext_full_at_cache_key_format(self):
656629 realm = "common" ,
657630 target = "r1/scope1 r1/scope2" ,
658631 ext_cache_key = ext_hash )
659- expected = "-login.windows.net-atext-d3adb33f-c0de-ed0c-c0de-deadb33fc0d3-common-r1/scope1 r1/scope2-latlwkpewb_a0rcsmjvkecqt0_huumkw4sflzociike "
632+ expected = "-login.windows.net-atext-d3adb33f-c0de-ed0c-c0de-deadb33fc0d3-common-r1/scope1 r1/scope2-bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi "
660633 self .assertEqual (expected , key )
661634
662- def test_atext_second_full_at_cache_key_format (self ):
663- """Second key-format vector (mirrors CacheKeyExtensionTests expectedCacheKey2) ."""
635+ def test_dotnet_style_second_cache_key (self ):
636+ """Reproduce CacheKeyExtensionTests expectedCacheKey2."""
664637 cache = TokenCache ()
665638 key_maker = cache .key_makers [TokenCache .CredentialType .ACCESS_TOKEN ]
666639 ext_hash = _compute_ext_cache_key ({"key3" : "value3" , "key4" : "value4" })
@@ -671,12 +644,13 @@ def test_atext_second_full_at_cache_key_format(self):
671644 realm = "common" ,
672645 target = "r1/scope1 r1/scope2" ,
673646 ext_cache_key = ext_hash )
674- expected = "-login.windows.net-atext-d3adb33f-c0de-ed0c-c0de-deadb33fc0d3-common-r1/scope1 r1/scope2-jjoe9jgfmdtnj0rzuetsqy7kzs2m1xfnjjxwsfxsrxq "
647+ expected = "-login.windows.net-atext-d3adb33f-c0de-ed0c-c0de-deadb33fc0d3-common-r1/scope1 r1/scope2-3-rg6_wyjx5bcy0c3cqq7gajtzgsqy3oxqpwj4y8k4u "
675648 self .assertEqual (expected , key )
676649
677650 def test_go_style_at_cache_key (self ):
678- """Reproduce the Go AccessToken.Key() format with Go's post-#629 hash:
679- 'testhid-env-atext-clientid-realm-user.read-{hash}'.
651+ """Reproduce the Go AccessToken.Key() *format* (segment layout):
652+ 'testhid-env-atext-clientid-realm-user.read-{hash}'. The hash follows our
653+ .NET-matching encoding (see class note on the Go #629 divergence).
680654 """
681655 cache = TokenCache ()
682656 key_maker = cache .key_makers [TokenCache .CredentialType .ACCESS_TOKEN ]
@@ -688,5 +662,5 @@ def test_go_style_at_cache_key(self):
688662 realm = "realm" ,
689663 target = "user.read" ,
690664 ext_cache_key = ext_hash )
691- expected = "testhid-env-atext-clientid-realm-user.read-latlwkpewb_a0rcsmjvkecqt0_huumkw4sflzociike "
665+ expected = "testhid-env-atext-clientid-realm-user.read-bns2ytmx5hxkh4fnfixridmezpbbayhnmuh6t4bbghi "
692666 self .assertEqual (expected , key )
0 commit comments