mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
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
54 lines
1.7 KiB
TypeScript
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';");
|
|
}
|
|
});
|
|
});
|