Skip to content

Commit 7a0b5a1

Browse files
committed
updated the cache key ext
1 parent 791161d commit 7a0b5a1

File tree

2 files changed

+125
-1
lines changed

2 files changed

+125
-1
lines changed

msal/token_cache.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
"scope",
4040
"username",
4141
"password",
42+
"client_assertion",
43+
"client_assertion_type",
44+
"assertion",
4245
})
4346

4447

@@ -89,6 +92,7 @@ class TokenCache(object):
8992

9093
class CredentialType:
9194
ACCESS_TOKEN = "AccessToken"
95+
ACCESS_TOKEN_EXTENDED = "atext" # Used when ext_cache_key is present (matches Go/dotnet)
9296
REFRESH_TOKEN = "RefreshToken"
9397
ACCOUNT = "Account" # Not exactly a credential type, but we put it here
9498
ID_TOKEN = "IdToken"
@@ -125,7 +129,9 @@ def __init__(self):
125129
"-".join([ # Note: Could use a hash here to shorten key length
126130
home_account_id or "",
127131
environment or "",
128-
self.CredentialType.ACCESS_TOKEN,
132+
# Use "atext" credential type when ext_cache_key is
133+
# present, matching MSAL Go and MSAL .NET behaviour.
134+
"atext" if ext_cache_key else "AccessToken",
129135
client_id or "",
130136
realm or "",
131137
target or "",

tests/test_token_cache.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)