* feat(auth): inject MCP_INSPECTOR_API_TOKEN into served index.html (#1378) Reloading the web client at the bare URL (no `?MCP_INSPECTOR_API_TOKEN=…` query string) with empty sessionStorage made every `/api/*` request 401 — the browser had no way to recover the backend's auth token. Embed the token into `index.html` on every page load so the browser no longer depends on the query string surviving navigation: - New shared helper `clients/web/server/inject-auth-token.ts` embeds `<script>window.__INSPECTOR_API_TOKEN__ = "…"</script>` (escaped against `</script>` injection; no-op when auth is dangerously omitted). - Dev: the Vite plugin injects via `transformIndexHtml`. - Prod: the Hono server injects on the `/` route. - `App.tsx` `getAuthToken()` now reads the injected global first, then the query string, then sessionStorage (both fallbacks preserved). - Shared global name lives in `INSPECTOR_API_TOKEN_GLOBAL` (`core/mcp/remote/constants.ts`). Tests: helper unit coverage + an integration test exercising the real prod server's `/` → `/api/*` flow (injected token authenticates; missing token 401s). AGENTS.md documents the token-recovery order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): inject token into prod SPA fallback + prime sessionStorage + no-store (#1378) Addresses code-review feedback on the token-injection PR: - Prod `server.ts` SPA deep-link fallback (e.g. `/oauth/callback`) previously served the raw index.html off disk via serveStatic, bypassing injection — so a bookmark/reload at a non-`/` route with empty sessionStorage would 401. Route the SPA fallback through the same `serveIndexHtml` (inject) helper; real static assets (paths with a dot) still serve verbatim. Dev already injected on every HTML serve via Vite `transformIndexHtml`. - `getAuthToken()` now persists the injected `window.__INSPECTOR_API_TOKEN__` to sessionStorage (not just the URL-param branch), priming the backstop for any later navigation that loses the global. - Injected HTML responses now send `Cache-Control: no-store`, so a page carrying a token isn't cached and served stale after a restart regenerates the token. Integration tests added: SPA fallback (`/oauth/callback`) carries the token, `Cache-Control: no-store` on injected HTML, real assets served verbatim, and unknown `/api` routes 404 rather than falling through to the HTML shell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e3b0b6a908
commit
881ba4b5e8
1 changed files with 372 additions and 29 deletions
|
|
@ -17,7 +17,10 @@ import type {
|
|||
ServerEntry,
|
||||
ServerType,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import { API_SERVER_ENV_VARS } from "@inspector/core/mcp/remote/constants.js";
|
||||
import {
|
||||
API_SERVER_ENV_VARS,
|
||||
INSPECTOR_API_TOKEN_GLOBAL,
|
||||
} from "@inspector/core/mcp/remote/constants.js";
|
||||
import { ManagedToolsState } from "@inspector/core/mcp/state/managedToolsState.js";
|
||||
import { ManagedPromptsState } from "@inspector/core/mcp/state/managedPromptsState.js";
|
||||
import { ManagedResourcesState } from "@inspector/core/mcp/state/managedResourcesState.js";
|
||||
|
|
@ -65,25 +68,44 @@ const redirectUrlProvider: RedirectUrlProvider = {
|
|||
getRedirectUrl: () => `${window.location.origin}/oauth/callback`,
|
||||
};
|
||||
|
||||
// Pull the dev-backend's auth token off the URL the launcher banner prints.
|
||||
// `npm run dev` opens `http://localhost:6274?MCP_INSPECTOR_API_TOKEN=…`;
|
||||
// every browser request to /api/* needs the same token in the
|
||||
// `x-mcp-remote-auth: Bearer …` header or the Hono backend returns 401.
|
||||
// Persist to sessionStorage so SPA navigations / OAuth round-trips don't
|
||||
// drop the token from the URL bar.
|
||||
// Recover the backend's auth token. Every browser request to /api/* needs it
|
||||
// in the `x-mcp-remote-auth: Bearer …` header or the Hono backend returns 401.
|
||||
// Three sources, in priority order:
|
||||
// 1. `window.__INSPECTOR_API_TOKEN__` — injected into index.html by the
|
||||
// backend on every page load (dev Vite plugin + prod Hono server). This
|
||||
// is the robust path: it survives a bare-URL reload, a bookmark, or a
|
||||
// cleared sessionStorage, none of which carry the query string.
|
||||
// 2. `?MCP_INSPECTOR_API_TOKEN=…` — the URL the launcher banner prints. Kept
|
||||
// as a fallback for pasted full URLs and older integrations.
|
||||
// 3. sessionStorage — backstop for SPA navigations / OAuth round-trips that
|
||||
// land without either of the above.
|
||||
// Both the injected global and the URL value are persisted to sessionStorage
|
||||
// so a later navigation that drops them (e.g. a deep-link load that wasn't
|
||||
// injected, or an iframe) still authenticates from the backstop.
|
||||
function getAuthToken(): string | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
const STORAGE_KEY = API_SERVER_ENV_VARS.AUTH_TOKEN;
|
||||
// Best-effort persistence — sessionStorage may be unavailable (privacy
|
||||
// mode, iframe sandboxing, etc.); the resolved value still works for the
|
||||
// current page load regardless.
|
||||
const persist = (token: string): void => {
|
||||
try {
|
||||
window.sessionStorage.setItem(STORAGE_KEY, token);
|
||||
} catch {
|
||||
// ignore — see note above
|
||||
}
|
||||
};
|
||||
const fromGlobal = (window as unknown as Record<string, unknown>)[
|
||||
INSPECTOR_API_TOKEN_GLOBAL
|
||||
];
|
||||
if (typeof fromGlobal === "string" && fromGlobal) {
|
||||
persist(fromGlobal);
|
||||
return fromGlobal;
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const fromUrl = params.get(API_SERVER_ENV_VARS.AUTH_TOKEN);
|
||||
if (fromUrl) {
|
||||
try {
|
||||
window.sessionStorage.setItem(STORAGE_KEY, fromUrl);
|
||||
} catch {
|
||||
// Best-effort persistence — sessionStorage may be unavailable
|
||||
// (privacy mode, iframe sandboxing, etc.); the URL value still
|
||||
// works for the current page load.
|
||||
}
|
||||
persist(fromUrl);
|
||||
return fromUrl;
|
||||
}
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue