Skip to content

Add refresh token cache partitioning support#6077

Merged
iNinja merged 11 commits into
mainfrom
iinglese/rt-cache-partition
Jun 25, 2026
Merged

Add refresh token cache partitioning support#6077
iNinja merged 11 commits into
mainfrom
iinglese/rt-cache-partition

Conversation

@iNinja

@iNinja iNinja commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends the existing access token (AT) cache partition mechanism (WithCachePartitionKey) to also partition refresh tokens (RTs). This allows callers who partition their AT cache to have matching RT isolation, enabling multi-resource token acquisition flows where a silent token request for a second resource can locate the RT from the first redemption via partition key.

Motivation

The AT partition feature (shipped in a previous release) allows callers to associate cache entries with an arbitrary partition key. However, refresh tokens were not partitioned, meaning that:

  1. A partitioned silent request could not find its corresponding RT
  2. Multi-resource flows (redeem code for resource A, then silently acquire token for resource B using the RT) failed because the RT from step 1 was not discoverable by partition

This change closes that gap.

Changes

MsalRefreshTokenCacheItem.cs

  • Added AdditionalCacheKeyComponents property (SortedList<string, string>)
  • Constructor accepts optional cacheKeyComponents parameter
  • InitCacheKey() appends a SHA256 hash of components to the cache key (mirrors AT pattern)
  • FromJObject / ToJObject: serialize components under the ext JSON field (same key as AT)
  • FRT guard: When FamilyId is set, partition components are silently ignored — Family Refresh Tokens are shared by design and must never be partitioned

TokenCache.ITokenCacheInternal.cs

  • Threads requestParams.CacheKeyComponents to the RT constructor in SaveTokenResponseAsync
  • Added FilterRefreshTokensByAdditionalKeyComponents method (symmetric with the existing AT filter)
  • Called in FindRefreshTokenAsync — but skipped when familyId is non-null to preserve FOCI fallback behavior

Backward Compatibility

  • Old MSAL reading new JSON: The ext field is not consumed by older versions, so it lands in AdditionalFieldsJson and survives round-trip serialization
  • New MSAL reading old JSON: Missing ext field results in null components — no partition filter applied, matching pre-existing behavior

Tests

10 new unit tests in RefreshTokenCachePartitionTests.cs:

Test What it verifies
RTWithPartition_CacheKeyIncludesHash Cache key includes SHA256 hash of components
RTWithoutPartition_CacheKeyUnchanged No components → original cache key format
FRT_NeverGetsPartitioned FamilyId set → components silently dropped
RTPartition_SerializationRoundTrip Components survive JSON serialize/deserialize
RTPartition_SerializationWithoutComponents No components → no ext field in JSON
OldMsalReadsNewJson_PartitionFieldPreservedAsAdditionalFields Unknown ext preserved in round-trip
NewMsalReadsOldJson_MissingPartitionFieldYieldsNullComponents Missing ext → null components
FilterRefreshTokensByAdditionalKeyComponents_FiltersCorrectly Filter keeps matching, removes non-matching
FilterRefreshTokensByAdditionalKeyComponents_NullComponents_KeepsAll Null components → no filtering
AcquireTokenByAuthCode_WithPartition_StoresPartitionedRT_Async Integration: auth code flow stores partitioned RT

No regressions: All existing AT partition tests (4), serialization tests (25+), and FOCI tests pass.

Extend the existing access token cache partition mechanism to refresh tokens,
allowing callers who use WithCachePartitionKey to also partition their RTs.

Changes:
- MsalRefreshTokenCacheItem: add AdditionalCacheKeyComponents property,
  SHA256 hash appended to cache key, JSON serialization under 'ext' field
- TokenCache: thread CacheKeyComponents to RT constructor, add
  FilterRefreshTokensByAdditionalKeyComponents filter in FindRefreshTokenAsync
- FRT guard: Family Refresh Tokens are never partitioned (shared by design);
  partition filter is skipped during FOCI lookups (familyId non-null)
- Backward compatible: old clients preserve the new 'ext' field via
  AdditionalFieldsJson round-trip; new clients handle missing 'ext' gracefully

Tests:
- 10 new unit tests covering cache key generation, FRT guard, serialization
  round-trip, backward compatibility, and integration with token acquisition
- All existing AT partition, serialization, and FOCI tests continue to pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@iNinja iNinja requested a review from a team as a code owner June 19, 2026 14:57
Copilot AI review requested due to automatic review settings June 19, 2026 14:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds refresh token (RT) cache partitioning aligned with the existing access-token partitioning model, enabling partitioned silent flows to locate the correct RT while preserving FRT (family refresh token) sharing semantics.

Changes:

  • Extends MsalRefreshTokenCacheItem to carry/serialize additional cache key components and incorporate them into the RT cache key.
  • Threads requestParams.CacheKeyComponents into RT creation and adds RT-side filtering by additional key components during RT lookup (skipping for FRT lookups).
  • Adds new unit tests covering RT key hashing, (de)serialization behavior, and auth-code flow storage scenarios.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
tests/Microsoft.Identity.Test.Unit/CacheTests/RefreshTokenCachePartitionTests.cs Adds unit tests validating RT partition hashing, serialization, and auth-code flow storage behaviors.
src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs Passes cache-key components into RT creation and filters RT candidates by additional key components during lookup (skipping for FRT lookups).
src/client/Microsoft.Identity.Client/Cache/Items/MsalRefreshTokenCacheItem.cs Adds RT partition components, cache-key suffixing, and ext serialization for RT partitioning (with FRT guard).

Comment thread src/client/Microsoft.Identity.Client/Cache/Items/MsalRefreshTokenCacheItem.cs Outdated
Comment thread tests/Microsoft.Identity.Test.Unit/CacheTests/RefreshTokenCachePartitionTests.cs Outdated
Comment thread tests/Microsoft.Identity.Test.Unit/CacheTests/RefreshTokenCachePartitionTests.cs Outdated
Comment thread tests/Microsoft.Identity.Test.Unit/CacheTests/RefreshTokenCachePartitionTests.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread tests/Microsoft.Identity.Test.Unit/CacheTests/RefreshTokenCachePartitionTests.cs Outdated
Comment thread tests/Microsoft.Identity.Test.Unit/CacheTests/RefreshTokenCachePartitionTests.cs Outdated
Fix cache key casing by passing partition hash through GetCredentialKey's
additionalKeys parameter so it gets lower-cased consistently.

Fix iOS keychain collision by including partition hash in GetiOSService()
for partitioned RTs.

Rewrite tests with meaningful assertions: compare partitioned vs
non-partitioned keys, simulate old MSAL by renaming ext field before
deserialization, and validate lower-cased hash expectations.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs Outdated
Copilot AI review requested due to automatic review settings June 22, 2026 11:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

iNinja and others added 2 commits June 22, 2026 16:09
Three new tests exercise the FindRefreshTokenAsync filter with
mixed partitioned and non-partitioned RTs in cache:

1. Silent with partition key finds partitioned RT and refreshes
2. Silent without partition key does not find partitioned RT (MsalUiRequired)
3. Mixed cache: partition isolates the correct RT for each flow

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Existing callers use WithCachePartitionKey for AT-only partition.
Adding RT partition to the same API would break them, so RT partition
is opt-in via a new bool parameter (default false).

WithCachePartitionKey(key, value) remains AT-only (backward compat).
WithCachePartitionKey(key, value, partitionRefreshToken: true) partitions
both AT and RT.

Adds a warning log when a non-partitioned silent call finds a partitioned
RT, helping developers catch mismatched partition usage.

Adds backward-compat test verifying silent calls without partitionRefreshToken
still find partitioned RTs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 23, 2026 11:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Comment thread src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs Outdated
iNinja and others added 2 commits June 23, 2026 12:34
The warning fires in the normal account transfer scenario where a
partitioned RT coexists with a regular OIDC RT after session expiry.
Not a misuse case, so the log would be noisy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use |= so that once partitionRefreshToken is set to true by any
WithCachePartitionKey call, a subsequent call without it cannot
accidentally reset it to false.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 23, 2026 11:37
Verifies that calling WithCachePartitionKey with partitionRefreshToken
true followed by a call without it does not reset the flag.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings June 23, 2026 11:43
Ensures debug logs show the actual cache key including the partition
hash, matching InitCacheKey and GetiOSService behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

This was referenced Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants