Web: concurrent OAuth flows to the same server overwrite each other's PKCE verifier (shared oauth.json keyed by server URL)
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- node.js, playwright, typescript
- 領域
- authentication, backend, testing, web-dev
調査の方向性
Start by reading clients/web/src/lib/remoteOAuthStorage.ts, core/auth/browser/storage.ts, and OAuthStorageBase.saveCodeVerifier to trace how OAuth state is stored and restored for /oauth/callback. Reproduce the overlap with two tabs using the provided steps, then inspect existing auth tests and add coverage for both flows completing with their own verifier. Done means concurrent flows no longer collide while the CLI/TUI stored-auth hand-off still works.
索引モデルが issue の本文から書いたものです。
説明
Which version line?
v2 — current (@modelcontextprotocol/inspector@latest)
Which client?
Web
Inspector version
2.11.0. The code involved is unchanged on v2/main as of 41018bea (2026-10-13).
Node version
v24.16.0 (npm i @modelcontextprotocol/[email protected], run with the same environment variables as the Docker image)
Operating system (and browser, for the web client)
macOS; Chrome (driven by Playwright)
Transport
Streamable HTTP
MCP server under inspection
An OAuth-protected streamable-HTTP server with Protected Resource Metadata → authorization server with Dynamic Client Registration, PKCE S256, token_endpoint_auth_method: none. Reproduced against a minimal mock authorization server and MCP server. The cause is in the Inspector's storage, so any server that uses PKCE triggers it.
Steps to reproduce
mcp-inspector --web(auth on, default settings).- Add an OAuth-protected streamable-HTTP server, or open the deep link
/?serverUrl=<url>&transport=http&autoConnect=<token>. - Tab A: click Connect and stay on the authorization server's login page.
- Tab B (same browser, same Inspector, same server): click Connect and complete the login.
- Back in tab A: complete the login.
The same happens when several people use one deployed Inspector (Docker image behind a reverse proxy) and log in to the same server at about the same time.
Expected behavior
Each authorization flow completes with its own PKCE verifier, whatever other flows are in flight for the same server. Both tabs end up connected.
Actual behavior
Tab B connects. Tab A's token exchange is rejected by the authorization server (invalid_grant, PKCE verification failed). Tab A shows the "Re-authentication required … PKCE verification failed" banner, and the server card is in the error state. It offers a Re-authenticate button; reloading and logging in again connects.
Cause: the web client's per-server OAuth state (codeVerifier, discoveryState, DCR client info, tokens) is held in the backend's single oauth.json, through RemoteOAuthStorage (/api/storage/oauth), keyed by server URL only (ServerOAuthState.codeVerifier, written by OAuthStorageBase.saveCodeVerifier). Two overlapping flows for the same server share one verifier slot. Tab B's flow overwrites tab A's verifier, and B's successful exchange then clears it. When tab A's callback page hydrates the store, the verifier it needs is gone.
Measured (automated, one browser context per user, logins taking 300–2300 ms at the authorization server):
- 1 user, 2 tabs: tab A failed in each run (2/2) (authorization server log:
authorize,token okfor B, thenauthorize,token rejected (PKCE)for A). - 6 users opening the same deep link at about the same time: 1 of 6 connects.
Related: because the store is keyed by server URL and shared by every browser using the backend, one browser's DCR client and, with the default MCP_INSPECTOR_PERSIST_TOKENS=all, its acquired tokens are also served to every other browser on that Inspector. MCP_INSPECTOR_PERSIST_TOKENS=none avoids the token part but not the verifier collision.
Logs, errors, or screenshots
- Before (2.11.0), tab A after step 5: re-authentication banner reading "Authentication for … needs attention. PKCE verification failed", server card in error, two
400POST /tokenrequests in the Network panel. - After (prototype below), tab A after step 5: server connected, "Authorization complete" notification.
- Failing token request:
grant_type=authorization_code,code=<issued for flow A>,code_verifier=<not A's>→400 {"error":"invalid_grant"}. GET /api/storage/oauthduring the overlap shows oneservers["<url>"].codeVerifiershared by both flows.
Already prototyped a fix?
Yes, as a local prototype to confirm the cause. It was built during a longer AI-assisted session rather than from one prompt, so below is a self-contained prompt that reproduces it, then a prompt for the fix we would actually suggest.
Prototype prompt (per-tab OAuth state; confirms the diagnosis):
In the v2 web client, OAuth state for a server (PKCE code verifier, discovery state, DCR client information, tokens) is persisted through
RemoteOAuthStorageinto the backend's sharedoauth.json, keyed only by server URL. Two authorization flows for the same server that overlap in time (two tabs, or two users of one deployed Inspector) overwrite each other'scodeVerifier, and the slower flow fails the token exchange with a PKCE error. Change the web client so this state is kept per browser tab: makegetWebRemoteOAuthStorage()inclients/web/src/lib/remoteOAuthStorage.tsreturn the existing sessionStorage-backedgetBrowserOAuthStorage()fromcore/auth/browser/storage.tsinstead of aRemoteOAuthStorage. Keep the backend connect path unchanged (the browser already sends its auth state with/api/mcp/connect). Add a test where two flows for the same server URL are started before either completes, and assert that both exchanges use their own verifier and both connect.
Suggested fix prompt (keeps the shared store from #1548):
The web client's in-flight PKCE state must not be shared between concurrent authorization flows. Today
OAuthStorageBase.saveCodeVerifier/getCodeVerifier/clearCodeVerifierkeycodeVerifier(and the discovery state saved with it) by server URL, in the shared remote store. Key the in-flight state by the auth session id thatgenerateOAuthState()already puts in the OAuthstateparameter (the 64-hexauthId), or keep it in the initiating tab's sessionStorage, so the/oauth/callbackcompletion reads the verifier for its ownstate. Only the outcome of a completed flow (client information, tokens) should be written to the shared server-URL-keyed store. Cover with tests: (1) two overlapping flows for the same server in two tabs both complete; (2) a callback whosestatehas no stored verifier fails with a clear error instead of using another flow's verifier; (3) the CLI/TUI--use-stored-authhand-off from a web-acquired token still works. Optionally, add a backend setting to keep all web OAuth state per tab, for deployments where one Inspector serves several people.
Before/after: before, any overlap fails all but the last flow to finish its login. With the prototype, every overlapping flow connects and GET /api/storage/oauth holds no codeVerifier.
How it was verified:
- Built 2.11.0 from the tag with the prototype applied (
npm install && npm pack, as in theDockerfile). - Ran both builds with the same automated browser tests (Chrome via Playwright, Streamable HTTP, a mock authorization server checking PKCE S256):
- 1 user, 2 overlapping tabs: 2.11.0 fails, the prototype gives 2/2 connected.
- 6 overlapping users: 2.11.0 gives 1/6, the prototype 6/6, each MCP session carrying its own user's token.
- Sequential logins, reload and reconnect, and MCP Apps rendering behaved the same with both builds.
Known trade-off of the prototype: each new tab runs its own OAuth flow, and its own DCR registration, as v1 did. The suggested fix avoids that.
Before you submit
- I searched existing issues and this is not a duplicate.
- This is not a security vulnerability report (those go through the private advisory process).
- 主要言語
- TypeScript
- スター
- 11.1k
- フォーク
- 1.6k
- 平均マージ
- 4時間 32分
- マージ済み PR(30日)
- 136
環境構築
- Dockerfile または Docker Compose ファイルあり
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
modelcontextprotocol/inspector のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
modelcontextprotocol/inspector#2725 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
modelcontextprotocol/inspector#2723 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 45/100
modelcontextprotocol/inspector#2727 ·
メンテナーはふだん 1 日以内に返信
-
insufficient_scope challenge retry doesn't detect expired access token, gives up after one identical retry対応中かも @its-nott-me が 3 日前に担当しました。 オープンbug
難易度 3/5 1〜2日 初心者へのやさしさ 50/100
modelcontextprotocol/inspector#2724 · コメント 4 件 ·
メンテナーはふだん 1 日以内に返信
-
Modern subscriptions: a subscriptions/listen refused with HTTP 403 is retried eight times in silenceオープンbug v2
難易度 4/5 3〜5日 初心者へのやさしさ 30/100
modelcontextprotocol/inspector#2722 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
modelcontextprotocol/inspector の issue をすべて見る
似ている issue
-
[Android] Nested transforms are composed in reverse order対応中かも @Danyalo が今日担当しました。 オープンRepro provided
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
software-mansion/react-native-svg#3090 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
radix-ui/primitives#4228 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 1/5 1時間未満 初心者へのやさしさ 95/100
rocketride-org/rocketride-server#2705 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
archon-ready archon-size-s archon-type-bug area: web
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
coleam00/Archon#4707 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
HarperFast/grafana-datasource#177 ·