Files
HealthLog/src/__tests__/proxy-mcp-authorize-csp.test.ts
T
Marc-André Bombeck 864566d882 fix(mcp): let the consent screen return to the connecting client
After Allow on the MCP OAuth consent screen, Chromium-based browsers
stayed on the page although the grant was issued. The page carried the
app-wide CSP form-action 'self', and Chromium applies form-action to the
redirect that follows the form post to the client's redirect_uri.

The authorize route now owns the CSP of its responses and the proxy
leaves that exact path alone. The consent page allows form-action for
its own origin plus the origin of the redirect URI the route has just
validated against the resolved client, and only https or http origins
qualify. Every other response from the route keeps form-action 'self'
with default-src 'none'. The redirect after the decision is now 303,
which tells every browser to follow a form POST with a GET. Refs #983
2026-09-15 14:06:27 +02:00

54 lines
1.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";
/**
* The MCP OAuth authorization endpoint owns its CSP: its consent form leads to
* the connecting client's redirect URI, and Chromium applies `form-action` to
* that redirect. Middleware headers override route headers, so the proxy must
* not set a CSP on exactly that path, and must keep setting it everywhere else,
* including the neighbouring OAuth endpoints.
*/
vi.mock("@/lib/process-type", () => ({
shouldRunWeb: () => true,
}));
import { proxy } from "../proxy";
beforeEach(() => {
vi.stubEnv("NODE_ENV", "production");
});
afterEach(() => {
vi.unstubAllEnvs();
});
function request(pathname: string): NextRequest {
return new NextRequest(`http://localhost${pathname}`);
}
describe("proxy CSP on the MCP authorize endpoint", () => {
it("leaves the CSP to the route on the authorize path", () => {
const res = proxy(
request("/api/mcp/oauth/authorize?client_id=x&redirect_uri=y"),
);
expect(res.headers.get("content-security-policy")).toBeNull();
// Every other security header still applies there.
expect(res.headers.get("x-frame-options")).toBe("DENY");
expect(res.headers.get("x-content-type-options")).toBe("nosniff");
});
it("keeps the app CSP on neighbouring and lookalike paths", () => {
for (const path of [
"/api/mcp/oauth/token",
"/api/mcp/oauth/register",
"/api/mcp/oauth/authorize/extra",
"/api/mcp/oauth/authorizex",
"/api/mcp/oauth",
]) {
const csp = proxy(request(path)).headers.get("content-security-policy");
expect(csp, path).toContain("form-action 'self';");
}
});
});