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
This commit is contained in:
Marc-André Bombeck committed 2026-09-15 14:06:27 +02:00
1 parent 7b7a77b292
commit 864566d882
6 files changed
+309 -10

No files matched your search

+12 -1
View File
@@ -683,7 +683,18 @@ export function proxy(request: NextRequest) {
isDev
? `default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; frame-src 'self' blob:; connect-src 'self'; font-src 'self';`
: `default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; frame-src 'self' blob:; connect-src 'self'${aiConnectSrc}${withingsConnectSrc}${whoopConnectSrc}${stravaConnectSrc}; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; worker-src 'self'; report-uri ${cspReportEndpoint}; report-to csp-endpoint;`;
response.headers.set("Content-Security-Policy", csp);
// The MCP OAuth authorization endpoint sets its own CSP. Its consent form
// posts to this origin and the server then redirects to the connecting
// client's `redirect_uri`; Chromium applies `form-action` to that redirect,
// so the app-wide `form-action 'self'` would strand the user after the
// grant. The route allows exactly the redirect origin it validated for the
// request (`src/lib/mcp/oauth/consent-csp.ts`). Middleware headers override
// route headers, so the proxy must leave the header unset on this exact
// path; every other header above still applies.
const isMcpAuthorizeRoute = pathname === "/api/mcp/oauth/authorize";
if (!isMcpAuthorizeRoute) {
response.headers.set("Content-Security-Policy", csp);
}
// Production-only headers. HSTS carries `preload` so the domain stays
// eligible for the Chromium preload list — closes the first-visit MITM