Skip to content

Add PartitionAppTokenCacheByAudience to partition the app token cache by resource - #3979

Merged
gladjohn merged 4 commits into
masterfrom
gladjohn/partition-app-token-cache-by-audience
Jul 27, 2026
Merged

Add PartitionAppTokenCacheByAudience to partition the app token cache by resource#3979
gladjohn merged 4 commits into
masterfrom
gladjohn/partition-app-token-cache-by-audience

Conversation

@gladjohn

@gladjohn gladjohn commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem

Apps that acquire app tokens (client credentials / AcquireTokenForClient) for many resources using the same client and tenant accumulate all of those tokens in a single {clientId}_{tenantId} app-token cache partition. MSAL's cache lookup is O(n) in the number of tokens in that partition, so a token-broker-style workload (one broker client acquiring tokens for many downstream audiences) can grow one partition very large and make every AcquireTokenForClient slow. Related: #3961.

What we've done so far

  • Don't short-circuit the in-memory token cache serialization provider #3970 (merged): stopped Microsoft.Identity.Web from short-circuiting the in-memory token cache, so the IMemoryCache serialization provider (with per-entry expiry and the cache-key partitioning hook) actually runs instead of MSAL's opaque static cache. That was the prerequisite for per-audience partitioning.

What this PR does

Adds an opt-in that puts the audience (resource) into the app-token cache key, so each resource gets its own partition and cache reads stay O(1).

  • New MicrosoftIdentityOptions.PartitionAppTokenCacheByAudience (bool, default false), config-bindable and flowed into MergedOptions (same pattern as EnableCacheSynchronization / UseFastUnboundedCache):
    "AzureAd": { "PartitionAppTokenCacheByAudience": true }
  • When enabled, TokenAcquisition.GetAuthenticationResultForAppInternalAsync derives the resource from the requested <resource>/.default scope and calls MSAL's public API:
    builder.WithCachePartitionKey("resource", resource);
    This is a non-protocol-affecting cache-key component (not sent to the token endpoint). Because MSAL builds the app-token cache key from these components on both the read (GetKeyFromRequest) and write (GetExternalCacheKeyFromResponse) paths, the single call partitions both MSAL's internal cache and the serialized IMemoryCache blob per resource, symmetrically.

Files

  • MicrosoftIdentityOptions.cs — new PartitionAppTokenCacheByAudience
  • MergedOptions.cs — flow it into MergedOptions
  • TokenAcquisition.cs — derive resource from scope + WithCachePartitionKey
  • PublicAPI/{NetCore,NetFramework}/PublicAPI.Unshipped.txt
  • Tests

Testing

Two behavior tests:

  • AppToken_PartitionByAudience_UsesSeparateCacheEntryPerResource — flag on, two resources → 2 IMemoryCache entries (one partition per resource).
  • AppToken_WithoutPartitionByAudience_SharesSingleCacheEntry — flag off, two resources → 1 shared entry (today's behavior).

Local unit suite green across TFMs (net462, net472, net8.0, net9.0, net10.0). Integration/E2E not run locally (require lab credentials).

Performance (measured locally through Microsoft.Identity.Web)

Measured IAuthorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync (in-memory cache) for one client/tenant acquiring app tokens for N distinct resources, with the flag off (current behavior after #3970) vs ON (this PR). Cache-hit = repeated acquisition of an already-cached resource; warm-up = time to acquire all N.

Resources (N) Partition Cache-hit mean Alloc / hit Warm-up (acquire N)
200 off 7.6 ms 2.7 MB 2.6 s
200 ON 0.20 ms 42 KB 0.08 s
1,000 off 21.2 ms 13.4 MB 23.6 s
1,000 ON 0.05 ms 42 KB 0.12 s
3,000 off 68.9 ms 40.3 MB 237 s
3,000 ON 0.05 ms 42 KB 0.39 s
  • Cache reads go O(n) → O(1): flag-off hit latency grows linearly with N; flag-on stays flat (~0.05 ms) regardless of N (~1,350× faster at N=3,000).
  • Allocations collapse: flag-off deserializes/rehydrates the whole partition blob on every hit (~40 MB/call at N=3,000); flag-on is flat ~42 KB/call (~970× less) — a large GC-pressure reduction under load.
  • Warm-up goes O(n²) → O(n): ~237 s vs ~0.39 s to acquire 3,000 resources (~600×).

(Measured with a throwaway local harness; not included in this PR.)

Notes

  • Requires MSAL ≥ 4.84.1 for WithCachePartitionKey; master is on 4.87.0.
  • Opt-in only; default behavior is unchanged.

🤖 Prototyped during an investigation with GitHub Copilot.

… by resource

Apps that acquire app tokens (client credentials) for many resources using the
same client and tenant accumulate all tokens in a single {clientId}_{tenantId}
cache partition, making AcquireTokenForClient cache lookups O(n) in the number of
resources.

Add a MicrosoftIdentityOptions.PartitionAppTokenCacheByAudience opt-in (config
bindable, flows into MergedOptions like UseFastUnboundedCache). When enabled,
TokenAcquisition derives the resource from the requested <resource>/.default scope
and calls MSAL's WithCachePartitionKey("resource", resource), which partitions both
MSAL's internal cache and the serialized in-memory cache blob per resource, keeping
reads O(1).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2d02b5ca-22b5-4b48-840a-de79786b3f9d
@gladjohn
gladjohn marked this pull request as ready for review July 27, 2026 17:10
@gladjohn
gladjohn requested a review from a team as a code owner July 27, 2026 17:10
@gladjohn
gladjohn enabled auto-merge (squash) July 27, 2026 18:20
@gladjohn
gladjohn merged commit dc242d6 into master Jul 27, 2026
9 checks passed
@gladjohn
gladjohn deleted the gladjohn/partition-app-token-cache-by-audience branch July 27, 2026 18:30
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.

3 participants