From 63a955386830f366ddda70ca0fdd52afd659869d Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Mon, 6 Jul 2026 17:11:52 -0400 Subject: [PATCH] auth: strip prompt=consent from initial authorize (auto-approve for connector) Claude forces the consent screen with prompt=consent; the consent endpoint's signed-query round-trip is brittle. Simpler: drop prompt=consent from the initial unsigned authorize so skipConsent applies and no consent page is shown. Also pass raw query bytes to the login/consent handlers. --- src/server.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/server.ts b/src/server.ts index bd77b85..caeb032 100644 --- a/src/server.ts +++ b/src/server.ts @@ -77,6 +77,11 @@ async function handleConsent(req: IncomingMessage, res: ServerResponse, rawSearc const server = createServer((req, res) => { const url = new URL(req.url ?? "/", "http://localhost"); + // Raw query bytes (the URL API re-encodes .search, which breaks Better Auth's + // signed oauth_query check on /consent). Take the query straight from req.url. + const rawUrl = req.url ?? "/"; + const qIndex = rawUrl.indexOf("?"); + const rawSearch = qIndex >= 0 ? rawUrl.slice(qIndex) : ""; if (url.pathname !== "/healthz") { // Request-level trace (path only, no query — avoids logging codes/tokens). // eslint-disable-next-line no-console @@ -89,13 +94,25 @@ const server = createServer((req, res) => { return; } if (url.pathname === "/login" && req.method === "GET") { - void startLogin(req, res, url.search); + void startLogin(req, res, rawSearch); return; } if (url.pathname === "/consent" && req.method === "GET") { - void handleConsent(req, res, url.search); + void handleConsent(req, res, rawSearch); return; } + // Claude sends prompt=consent on the initial authorize, which forces the + // consent screen even with skipConsent. For a personal connector the login is + // the authorization, so strip it from the initial (unsigned) request — the + // signed resume after login has a `sig` and is left untouched. + if ( + url.pathname === "/api/auth/oauth2/authorize" && + !url.searchParams.has("sig") && + url.searchParams.get("prompt") === "consent" + ) { + url.searchParams.delete("prompt"); + req.url = url.pathname + (url.searchParams.toString() ? `?${url.searchParams}` : ""); + } // Everything else -> Better Auth (async handler). void handler(req, res); });