Skip to content

fix(auth): deduplicate concurrent token refresh requests#1813

Open
Aboudjem wants to merge 2 commits intomodelcontextprotocol:mainfrom
Aboudjem:fix/auth-refresh-race-condition
Open

fix(auth): deduplicate concurrent token refresh requests#1813
Aboudjem wants to merge 2 commits intomodelcontextprotocol:mainfrom
Aboudjem:fix/auth-refresh-race-condition

Conversation

@Aboudjem
Copy link
Copy Markdown

Summary

  • Fix race condition in auth() where concurrent 401 handlers each independently refresh the same token, causing rotating refresh tokens to be reused and triggering RFC 6819 §5.2.2.3 replay detection
  • Add promise-based deduplication using a Map<OAuthClientProvider, Promise<AuthResult>> — when a refresh is already in-flight for a provider, concurrent callers await the existing promise instead of starting a new one
  • Add 4 tests covering: concurrent deduplication (single token request), sequential calls (separate requests), error propagation to all callers, and independent providers not being deduplicated

Problem

When two or more requests for the same OAuth MCP server arrive in parallel:

  1. Both detect a 401 and call auth()
  2. Both independently call refreshAuthorization() with the same refresh token
  3. With rotating refresh tokens (Atlassian, Asana, etc.), the first refresh consumes the token and issues a new one
  4. The second refresh replays the now-consumed token, triggering the authorization server's replay detection
  5. The server revokes the entire token family, permanently breaking the connection

Solution

Store the in-flight auth() promise in a module-level Map keyed by provider instance. If a concurrent call arrives for the same provider, it awaits the existing promise. The entry is cleaned up in a finally block so subsequent refreshes proceed normally.

This is the same pattern used by msal-browser and axios-auth-refresh.

Test plan

  • New test: concurrent auth calls result in exactly 1 token refresh request
  • New test: sequential auth calls each trigger their own refresh
  • New test: errors propagate correctly to all concurrent callers
  • New test: different providers are not deduplicated
  • All 260 existing client tests pass
  • TypeScript typecheck passes

Closes #1760

When multiple requests detect a 401 simultaneously, they each call auth()
which races to refresh using the same refresh token. With OAuth servers
that use rotating refresh tokens (e.g. Atlassian, Asana), this triggers
RFC 6819 §5.2.2.3 replay detection — the second refresh invalidates the
first's new token, revoking the entire token family.

Add a Map-based singleton guard that stores the in-flight auth promise
keyed by provider. Concurrent callers await the existing promise instead
of starting a new refresh. The promise is cleared on resolve/reject so
future refreshes proceed normally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Aboudjem Aboudjem requested a review from a team as a code owner March 29, 2026 09:52
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 29, 2026

⚠️ No Changeset found

Latest commit: 94f69f3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 29, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1813

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1813

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@1813

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@1813

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@1813

commit: 94f69f3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

Race condition in auth() causes refresh token invalidation when rotating tokens are used

1 participant