Skip to content

fix: deduplicate concurrent OAuth refresh token exchanges#1787

Open
claygeo wants to merge 1 commit intomodelcontextprotocol:mainfrom
claygeo:fix/auth-refresh-race-condition
Open

fix: deduplicate concurrent OAuth refresh token exchanges#1787
claygeo wants to merge 1 commit intomodelcontextprotocol:mainfrom
claygeo:fix/auth-refresh-race-condition

Conversation

@claygeo
Copy link
Copy Markdown

@claygeo claygeo commented Mar 27, 2026

Problem

When multiple parallel MCP requests receive 401 responses simultaneously, each independently calls onUnauthorizedhandleOAuthUnauthorizedrefreshAuthorization with the same refresh token. OAuth providers using rotating refresh tokens (Atlassian, Asana, and others per RFC 6819 §5.2.2.3) detect the second use of a refresh token as a replay attack and revoke the entire token family, logging the user out.

How it happens

Request A → 401 → onUnauthorized → refreshAuthorization(token_v1) → gets token_v2
Request B → 401 → onUnauthorized → refreshAuthorization(token_v1) → REPLAY DETECTED → all tokens revoked

There is no concurrency guard in adaptOAuthProvider or handleOAuthUnauthorized — each 401 handler runs independently.

Solution

Add promise coalescing in adaptOAuthProvider(): the first onUnauthorized call stores its refresh promise. All concurrent 401 handlers await the same promise instead of initiating separate refresh exchanges. The promise is cleared after completion (success or failure) so future refreshes proceed normally.

let inflightRefresh: Promise<void> | undefined;
// ...
onUnauthorized: async ctx => {
    if (inflightRefresh) {
        return inflightRefresh;
    }
    inflightRefresh = handleOAuthUnauthorized(provider, ctx)
        .finally(() => { inflightRefresh = undefined; });
    return inflightRefresh;
}

This is the standard pattern for deduplicating concurrent token refresh operations, used by libraries like axios-auth-refresh, msal-browser, and Apollo Client.

Changes

  • packages/client/src/client/auth.ts — Added inflightRefresh promise coalescing in adaptOAuthProvider()

Test Plan

  • Single 401 → refresh succeeds → new token used (existing behavior preserved)
  • Two concurrent 401s → only one refreshAuthorization call made → both requests retry with new token
  • Refresh failure → promise cleared → next 401 retries fresh
  • Sequential 401s (after first completes) → each triggers its own refresh (promise was cleared)

Closes #1760

When multiple parallel requests receive 401 responses, each
independently calls onUnauthorized -> handleOAuthUnauthorized ->
refreshAuthorization with the same refresh token. OAuth providers
using rotating refresh tokens (Atlassian, Asana, per RFC 6819
5.2.2.3) detect the second use as a replay attack and revoke the
entire token family, logging the user out.

The fix adds promise coalescing in adaptOAuthProvider: the first
401 handler stores its refresh promise, and all concurrent 401s
await the same promise instead of initiating separate refresh
exchanges. The promise is cleared after completion (success or
failure) so future token refreshes proceed normally.

Closes modelcontextprotocol#1760
@claygeo claygeo requested a review from a team as a code owner March 27, 2026 15:29
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 27, 2026

⚠️ No Changeset found

Latest commit: f7e365c

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 27, 2026

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/server

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

@modelcontextprotocol/express

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: f7e365c

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