Fix cache key hash collision: length-prefix extended cache key components - #946
Merged
Merged
Conversation
…ents
The extended cache key serialization concatenated sorted key+value pairs
with no delimiters, which is not injective: distinct component sets such as
{fmi_path: 'value'} and {fmi_pat: 'hvalue'} serialized to the same string
and hashed to the same cache key, causing cache-slot collisions and
redundant token re-fetches.
Adopt MSAL Go's length-prefixed (netstring) encoding
(<byteLen(key)>:<key><byteLen(value)>:<value> per sorted key, UTF-8 byte
lengths), which is injective and byte-identical across the MSAL SDK family.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6efa99f2-b923-4ea8-b5b8-40696d1470a5
Contributor
There was a problem hiding this comment.
Pull request overview
This pull request fixes extended token cache key hash collisions by changing _compute_ext_cache_key to use an injective, UTF-8 byte-length-prefixed (“netstring”-style) serialization before SHA-256 + base64url encoding, aligning with the cross-SDK scheme MSAL is converging on.
Changes:
- Update
msal/token_cache.py::_compute_ext_cache_keyto length-prefix each key/value using UTF-8 byte lengths prior to hashing. - Update cross-MSAL compatibility tests to the new expected hashes and wording.
- Add collision-resistance tests (boundary ambiguities, delimiter-like content, UTF-8 byte-length cases, fuzz/injectivity checks, golden vectors).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
msal/token_cache.py |
Implements length-prefixed serialization to prevent extended cache key hash collisions. |
tests/test_token_cache.py |
Updates expected hashes and adds new tests to validate injectivity and collision resistance. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Skip same-key pair combinations explicitly (a dict comprehension would silently overwrite and collapse intended 2-entry cases), and assert on len(seen) -- the count of distinct component sets exercised -- instead of a raw iteration counter. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6efa99f2-b923-4ea8-b5b8-40696d1470a5
Avery-Dunn
approved these changes
Jul 22, 2026
Reword the token_cache and test docstrings to state that the length-prefix scheme makes the *serialization* injective (distinct inputs cannot produce the same pre-hash string), rather than implying absolute impossibility of a collision at the SHA-256 hash layer. Also quote the dict-literal examples as valid Python. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6efa99f2-b923-4ea8-b5b8-40696d1470a5
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
tests/test_token_cache.py:777
- The phrase "code-point length would make these ambiguous" is misleading here: the two strings differ regardless of which length you compute. The intent is to assert the implementation uses UTF-8 byte length (not len(str)); rewording this comment would make that clearer.
# A concrete boundary pair the two length schemes disagree on:
# code-point length would make these ambiguous, byte length does not.
Member
|
Please update the milestone. |
Bogdan Gavril (bgavrilMS)
approved these changes
Jul 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The FMI / agent-identity feature hashes extended cache-key components (extra body params such as
fmi_path, and cache-key-only pseudo-params likeclient_claims) into the token cache key via_compute_ext_cache_keyinmsal/token_cache.py.The previous serialization concatenated the sorted
key + valuepairs with no delimiters, which is not injective. Semantically different component sets serialize to the same string and therefore hash to the same cache key:{fmi_path: "value"}and{fmi_pat: "hvalue"}→ bothfmi_pathvalue{a: "b", cd: "e"}and{ab: "c", d: "e"}→ bothabcdeImpact: a cache-slot collision — one token entry overwrites/evicts another, forcing a redundant token re-fetch.
Fix
Replace the delimiter-less concatenation with an injective length-prefixed ("netstring") encoding, matching MSAL Go's
CacheExtKeyGenerator(microsoft-authentication-library-for-go#629). For each key sorted ascending, append<byteLen(key)>:<key><byteLen(value)>:<value>and concatenate; then SHA256 → base64url (no padding) → lowercase, exactly as before.len(s.encode("utf-8"))), not the Unicode code-point count, so the hash stays byte-identical to MSAL Go/.NET/Java/JS as the SDK family converges on this scheme._EXT_CACHE_KEY_EXCLUDED_FIELDS, truthy values) and the empty-input early return are unchanged.Tests
TestCrossMsalCacheKeyCompatibilityfor the new encoding and reworded it to reflect convergence on the shared MSAL length-prefix hash. The cache-key format (atextsegment layout) assertions are unchanged.TestExtCacheKeyCollisionResistancecovering:<len>:<data>delimiter characters;:,|,\, empty string,é, emoji, combining accent) asserting no two distinct component dicts collide;évse+ combining accent must not collide — proving byte length, not code-point length, is used);python -m pytest tests/test_token_cache.py→ 52 passed. (tests/test_fmi_e2e.pyis a live lab test requiring theazurepackage + credentials and only asserts that keys differ; no hard-coded hashes.)Compatibility note
Because the serialization changes, the computed
ext_cache_keyhashes change. On upgrade, previously cached FMI / extended-cache-key access tokens will not be found under their new keys, causing a one-time cache miss (a single extra token fetch per affected entry). This is benign and self-healing — no action required. Only extended-cache-key entries (e.g. FMI) are affected; regular access tokens are unchanged.Notes
CHANGELOG.md, so no changefile is added.