claude-code - 💡(How to fix) Fix MCP: OAuth authorize requests intermittently corrupted [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#54727Fetched 2026-04-30 06:37:41
View on GitHub
Comments
1
Participants
2
Timeline
7
Reactions
0
Author
Timeline (top)
labeled ×5commented ×1cross-referenced ×1

Error Message

Error Messages/Logs

Root Cause

The current behavior surfaces only as needs-auth in Claude Code, with no diagnostic information for the user. We had to instrument server-side IdP audit logs to root-cause this — not realistic for most users, who just see "I have to re-auth my MCP again."

RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code

What's Wrong?

When Claude Code talks to remote MCP servers via OAuth (RFC 9728 protected-resource discovery + Dynamic Client Registration + PKCE), it intermittently sends malformed authorize requests to the IdP. The IdP rejects them, Claude marks the server needs-auth, and the user has to re-authenticate — over and over, every 2–4 days.

What the corruption looks like

Captured from real IdP audit logs. The Raw parameters in the rejected /connect/authorize requests show clear bit-rot, not protocol-level mistakes:

"code_challenge_method_method": "S256" ← extra "_method" suffix "code_challenge _method": "S256" ← whitespace injected mid-name "code_challenge": "K4wB5xvS" ← truncated (should be 43+ chars) "code_challenge_method": "S" ← truncated to 1 char "resource": "https://my-mcp/ .example.com/mcp" ← whitespace inside hostname "scope": (missing entirely) "redirect_uri": "http://localhost/:" ← truncated, no port

These are not user input. Claude Code constructs these values internally from PKCE state and DCR registration data. There is no legitimate code path that produces code_challenge_method=S or a hostname with two spaces in the middle.

Two separate failure modes observed

Same client, same IdP, same user. Both produce the user-visible symptom of being asked to re-auth, but they happen at different stages:

1. Authorize request corruption (above)

Happens at /connect/authorize, before any token is issued. IdP rejects with messages like "code_challenge_method of plain is not allowed" (because code_challenge_method got renamed by the corruption and the IdP defaulted to plain), or "redirect_uri is missing or too long", or "scope is missing".

2. Cross-client code/token redemption

Happens at /connect/token. The OAuth request is well-formed, but the client_id Claude Code presents doesn't match the client_id the code or refresh token was originally issued to. IdP correctly rejects with invalid_grant and the message:

"Client is trying to use a code from a different client { clientId: "", codeClient: "" }"

This indicates Claude Code's local OAuth state has drifted: the stored DCR client_id and the stored refresh_token (or auth code)belong to different DCR registrations.

Likely root causes

Both failure modes are consistent with Claude Code's OAuth path not being safe under concurrent execution — multiple Claude Code processes share user-level OAuth state, and multiple MCP servers in one process can do OAuth flows concurrently.

Specifically:

  • Storage race → cross-client redemption. DCR client_id, refresh_token, and authorization code are written non-atomically. Two concurrent flows interleave their writes, and the on-disk record ends up with client_id from one flow and refresh_token from another.
  • In-memory race / shared buffer → authorize request corruption. The pattern of "1-character truncation" + "bytes from another string injected mid-value" + "whitespace appearing inside fixed strings" looks like a non-thread-safe buffer being reused while a concurrent flow is still writing to it.
  • WWW-Authenticate parser fragility is a contributing trigger — when an MCP server emits multiple WWW-Authenticate headers (which is RFC-compliant; some popular auth middleware does this by default), Claude Code's parser mis-splits the folded value and produces the mangled parameter names.

Recoverability

Failure happens atRecoverable?
Initial auth code exchangeYes — Claude opens a fresh authorize flow, eventually wins the race, state self-heals. User may not
notice.
Refresh token requestNo — the stored refresh token is now bound to a client_id Claude no longer has. Every future refresh
fails. Only fix is full interactive re-auth.

The "every 2–4 days" symptom comes from the refresh path: Claude Code refreshes hourly, and once a single refresh request is corrupted (or the stored state has drifted), the server is locked out until the user re-authenticates.

Suggested fixes

  1. Atomic storage of OAuth state. Write (client_id, refresh_token, code) as a single atomic unit — temp file + rename, or a file lock — so concurrent processes can't interleave.
  2. Serialize OAuth flows within a single Claude Code process when they touch the same user-level state.
  3. Audit the authorize URL builder for thread-safety. The string corruption pattern strongly suggests a shared mutable buffer.
  4. Harden the WWW-Authenticate parser to handle multiple/folded WWW-Authenticate headers per RFC 7235 without producing mangled parameter names.
  5. Detect drift on read — if the stored client_id doesn't match the issuer of the stored refresh token, trigger interactive re-auth proactively instead of sending a doomed refresh request.

Why this matters

The current behavior surfaces only as needs-auth in Claude Code, with no diagnostic information for the user. We had to instrument server-side IdP audit logs to root-cause this — not realistic for most users, who just see "I have to re-auth my MCP again."

Environment

  • Claude Code CLI (latest)
  • macOS, multiple Claude Code processes running concurrently
  • 3+ remote MCP servers configured at user level, all using OAuth 2.1 + DCR + PKCE
  • IdP refresh token lifetime: 30-day absolute / 15-day sliding (so this is not a token-expiry issue)
  • Access token lifetime: 1 hour

What Should Happen?

  1. Concurrent Claude Code processes (and concurrent OAuth flows for different MCP servers within the same process) should never produce a corrupted /connect/authorize request. Field names, PKCE values, redirect URIs, and scopes must be transmitted byte-for-byte as Claude Code constructed them.

  2. Claude Code's local OAuth state must remain internally consistent: the stored DCR client_id, the stored refresh_token, and any cached authorization code must always belong to the same registration. Concurrent writes from multiple processes must not be able to leave them out of sync.

  3. When the stored refresh token cannot be used (e.g., it belongs to a client_id Claude Code no longer has), Claude Code should detect the drift before sending a doomed refresh request and trigger a fresh interactive re-auth — not silently fail and mark the server needs-auth.

  4. With a 30-day absolute / 15-day sliding refresh token lifetime configured on the IdP, a user who actively uses an MCP server should not need to re-authenticate more than once every ~15 days. Currently it's every 2–4 days.

  5. When a refresh does fail, the user-facing needs-auth state should include a diagnostic (or at minimum a debug log entry) explaining why — invalid_grant, client mismatch, malformed request, etc. Today there is no signal at all.

Error Messages/Logs

Steps to Reproduce

We do not have a tight, deterministic repro — the bug is a race condition and surfaces intermittently. The following setup reliably produces the failure within a few days of normal use:

  1. Configure Claude Code with 2 or more remote MCP servers at user level (~/.claude.json), each using OAuth 2.1 + Dynamic Client Registration + PKCE against the same IdP.
  2. Authenticate each server interactively so refresh tokens are stored locally.
  3. Use Claude Code normally — open multiple terminal sessions / projects concurrently so several claude processes are alive at the same time.
  4. Let access tokens expire (default 1 hour) so Claude Code performs background refreshes. With multiple processes alive, refreshes for different MCPs will overlap.
  5. Within 2–4 days, at least one MCP server will transition to needs-auth and require interactive re-authentication, despite the refresh token still being well within its sliding/absolute lifetime.

What we observed in IdP audit logs during the failure window

Capturing /connect/authorize and /connect/token requests on the IdP side showed two failure patterns occurring repeatedly:

  • Malformed authorize requests with mangled parameter names (code_challenge_method_method), whitespace injected mid-value (https://my-mcp .example.com/mcp), or values truncated to 1–12 characters when they should be 43+.
  • Cross-client code/token redemption — Claude Code presents an auth code or refresh token at /connect/token while claiming to be a client_id different from the one the IdP issued the credential to.

Both correlate with periods of concurrent Claude Code activity (multiple processes, near-simultaneous token refreshes).

Frequency

  • 1 affected MCP server: re-auth every ~7–10 days
  • 2 affected MCP servers: re-auth every ~3–5 days per server
  • 3+ affected MCP servers: re-auth every 2–4 days per server

The frequency scaling with the number of installed MCP servers is itself evidence that concurrent OAuth flows are the trigger.

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

No response

Claude Code Version

4,7

Platform

Google Vertex AI

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

No response

extent analysis

TL;DR

To fix the intermittent malformed authorize requests and cross-client code/token redemption issues in Claude Code, implement atomic storage of OAuth state, serialize OAuth flows, and harden the WWW-Authenticate parser.

Guidance

  1. Implement atomic storage: Write (client_id, refresh_token, code) as a single atomic unit to prevent concurrent processes from interleaving writes.
  2. Serialize OAuth flows: Ensure that OAuth flows within a single Claude Code process do not touch the same user-level state concurrently.
  3. Harden the WWW-Authenticate parser: Modify the parser to handle multiple/folded WWW-Authenticate headers per RFC 7235 without producing mangled parameter names.
  4. Detect drift on read: Trigger interactive re-auth proactively if the stored client_id doesn't match the issuer of the stored refresh token.
  5. Audit the authorize URL builder: Verify that the builder is thread-safe to prevent string corruption.

Example

No specific code snippet is provided, as the issue requires a comprehensive review of Claude Code's OAuth implementation.

Notes

The provided information suggests that the issue is related to concurrent execution and shared state. Implementing the suggested fixes should resolve the intermittent issues. However, a thorough review of the code and testing are necessary to ensure the fixes are effective.

Recommendation

Apply the suggested workarounds, specifically implementing atomic storage and serializing OAuth flows, to address the root causes of the issue. This approach should mitigate the problems until a more comprehensive solution can be implemented.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING