MCP: OAuth authorize requests intermittently corrupted

Resolved 💬 3 comments Opened Apr 29, 2026 by bluedog13 Closed May 3, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] 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 at | Recoverable? |
|---|---|
| Initial auth code exchange | Yes — Claude opens a fresh authorize flow, eventually wins the race, state self-heals. User may not
notice. |
| Refresh token request | No — 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.

  1. Serialize OAuth flows within a single Claude Code process when they touch the same user-level state.
  2. Audit the authorize URL builder for thread-safety. The string corruption pattern strongly suggests a shared mutable buffer.
  3. Harden the WWW-Authenticate parser to handle multiple/folded WWW-Authenticate headers per RFC 7235 without producing mangled

parameter names.

  1. 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.
  1. 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.
  1. 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.
  1. 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.
  1. 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_

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗