modelcontextprotocol / modelcontextprotocol/typescript-sdk

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

Open
#1,760 6 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

auth bug fix proposed P2 ready for work
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 15h
Merged PRs (30d)
4

Description

Describe the bug
When two requests for the same OAuth MCP server arrive in parallel, both calls independently detect a 401, call auth(), and race 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 authorization server detects that a consumed refresh token was reused and revokes the entire token family, permanently breaking the connection until the user manually re-authorizes.

To Reproduce

  1. Set up an MCP server connection using an OAuth provider with rotating refresh tokens (e.g. Atlassian)
  2. Let the access token expire, or manually add a bad token
  3. Send two parallel requests to the MCP server before either completes the refresh, this will have to be quick so use a simple script to make 2 immediate calls
curl ... &
curl ... &
wait
  • Both requests call auth() simultaneously, each POSTing to the token endpoint with the same refresh token
  • The OAuth server receives two requests with the same refresh token — the second is a replay

Expected behavior
Only one token refresh should occur. The second concurrent request should wait for the in-flight refresh to complete and reuse the resulting tokens, not trigger its own parallel refresh.

Logs

InvalidGrantError: Invalid refresh token
    at parseErrorResponse (client/auth.ts:292:16)
    at refreshAuthorization (client/auth.ts:1022:15)
    at authInternal (client/auth.ts:419:31)
    at auth (client/auth.ts:325:20)
    at StreamableHTTPClientTransport.send (client/streamableHttp.ts:442:36)

Additional context
Root cause is in auth() in client/auth.ts — there is no concurrency guard preventing two simultaneous refresh flows for the same provider.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in client/auth.ts, especially auth(), authInternal, and refreshAuthorization, where the logs show the concurrent refresh flow. Reproduce the issue with two parallel requests against a server using rotating refresh tokens. Done means one refresh occurs and the second request waits for and reuses the resulting tokens without invalidating the token family.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, authentication
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.