Skip to content

Make auth provider token caches thread-safe (#1595)#1708

Open
tarekgh wants to merge 2 commits into
modelcontextprotocol:mainfrom
tarekgh:fix-thread-safe-auth-token-caches
Open

Make auth provider token caches thread-safe (#1595)#1708
tarekgh wants to merge 2 commits into
modelcontextprotocol:mainfrom
tarekgh:fix-thread-safe-auth-token-caches

Conversation

@tarekgh

@tarekgh tarekgh commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #1595.

Problem

ClientOAuthProvider and IdentityAssertionGrantProvider lazily populated mutable cache fields (tokens, server metadata, resolved endpoints) during 401 handling with no synchronization. When the same provider handles concurrent in-flight requests, this caused redundant token exchanges and races on the cache fields.

Fix

  • Coalesce token acquisition in both providers behind a SemaphoreSlim so only one caller performs the exchange/refresh while others await and reuse the result.
  • Valid cached tokens keep a lock-free fast path.
  • Thread the token that triggered a 401 through the flow so a stale-but-unexpired token still forces a real refresh (instead of being served back).
  • Guard InvalidateCache and document the concurrency contract on ITokenCache.

Tests

  • Added a deterministic concurrency test asserting a single exchange runs for multiple concurrent callers.
  • Existing auth unit and AspNetCore OAuth tests pass; full solution builds clean with no warnings.

ClientOAuthProvider and IdentityAssertionGrantProvider lazily populated
mutable cache fields during 401 handling with no synchronization. Under
concurrent in-flight requests on a shared handler this caused redundant
token exchanges and races on the cache fields.

Coalesce token acquisition in both providers behind a SemaphoreSlim so
only one caller performs the exchange or refresh while others await and
reuse the result. Valid cached tokens still take a lock-free fast path.
Thread the token that caused a 401 through so a stale-but-unexpired token
still forces a real refresh. Guard InvalidateCache and document the
concurrency contract on ITokenCache.

Adds a deterministic concurrency test verifying a single exchange runs
for multiple concurrent callers.

Copilot-Session: 334e4d21-b783-46ae-8dc6-3c3b5defbe37
@tarekgh
tarekgh requested a review from halter73 July 16, 2026 22:38
@tarekgh tarekgh self-assigned this Jul 16, 2026
@tarekgh

tarekgh commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

CC @jeffhandley

)

The post-lock cache re-check only reused a freshly cached token when the
challenge carried a token (usedAccessToken is not null). During a
cold-start connect the client can issue concurrent requests that all send
no token, so each saw a null usedAccessToken, skipped the re-check and ran
a full interactive authorization, producing redundant token exchanges
(observed in CI as two authorization requests on a single connect).

When no token was sent, any valid token now present in the cache was
obtained by another caller and is safe to reuse, so drop the null guard.
The remaining checks still prevent replaying a rejected token (the cached
token must differ from the one that produced the challenge) and still run
the step-up flow for 403 insufficient_scope challenges.

Copilot-Session: 334e4d21-b783-46ae-8dc6-3c3b5defbe37
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.

Token/metadata caches in auth providers are not thread-safe

1 participant