modelcontextprotocol / modelcontextprotocol/csharp-sdk

Interactive OAuth flow is aborted by DiscoverProbeTimeout during dual-path connect, then fails with an authorization state mismatch

Open
#1,830 2 comments 0 reactions 1 assignee View on GitHub

@PederHP is already working on this.

Since Aug 20, 2026.

area-auth bug P2
Dominant language
C#
Stars
4.5k
Forks
814
Avg merge
9d 19h
Merged PRs (30d)
4

Description

Summary

When connecting to an OAuth-protected server with the default dual-path connect (ProtocolVersion unset), an interactive authorization flow started by the server/discover probe's 401 challenge is silently destroyed by McpClientOptions.DiscoverProbeTimeout (default 5s), and the connect then reliably fails with:

ModelContextProtocol.McpException: Failed to handle unauthorized response with 'Bearer' scheme. The authorization response state did not match the state sent in the authorization request.
Sequence (observed against a production MCP server, SDK 2.2.0)
  1. The client sends the server/discover probe. The server answers 401, which enters ClientOAuthProvider: metadata discovery, DCR, then AuthorizationCallbackHandler is invoked. The host presents the authorization URL (state S1) to the user, who starts logging in.
  2. After 5 seconds, DiscoverProbeTimeout elapses. This timeout exists for servers that silently drop unknown methods — but here the cancellation propagates through HandleUnauthorizedResponseAsync into the pending interactive flow and aborts it, even though the user is mid-login in a browser the SDK cannot close.
  3. The client falls back to initialize, which draws another 401 and starts a second interactive flow with a fresh state S2 and PKCE verifier. The host has no new popup to show — the user is still completing flow 1.
  4. The user finishes logging in; the redirect arrives carrying S1. The second flow expects S2ValidateStateResponse throws the state-mismatch error above. The user-approved authorization code is unusable by flow 2 anyway (different PKCE verifier), so there is no way to recover the flow the user actually completed.

A second, related problem: the whole interactive login is bounded by InitializationTimeout (default 60s), which a real login (with MFA etc.) easily exceeds. That part is at least configurable, but nothing in the docs points hosts at it.

Expected behavior

Canceling the request whose challenge started an interactive authorization should not abort the flow itself — the user-facing browser flow is already in progress and only one flow can ever match the redirect the user completes. A subsequent challenge (from the initialize fallback, or any concurrent request) should join the in-flight flow and reuse its result instead of starting a competing flow the user never sees.

Suggested fix

In ClientOAuthProvider, memoize the in-flight authorization-code flow and detach it from the triggering request's cancellation token (bounding it by provider disposal instead); challenge handlers await it with their own token via Task.WaitAsync. The existing _tokenAcquisitionLock + cached-token re-check already handles the "another caller acquired the token" case once the flow survives.

Docs for DiscoverProbeTimeout / InitializationTimeout should also mention the interactive-authorization interplay.

I have a PR ready with this change plus regression tests (deterministic cancellation-survival test, an end-to-end probe-timeout scenario test, and a transport-dispose cancellation test).

Workaround (current releases)

For interactive connects, set DiscoverProbeTimeout = Timeout.InfiniteTimeSpan and raise InitializationTimeout above the host's authorization window.

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.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.