59 lines · 2.7 KB · TypeScript
RawDownload
// Force a human approval screen on every MCP OAuth connection.//// Better Auth's MCP authorize endpoint only shows the consent page when the// request carries `prompt=consent` (it otherwise auto-issues an authorization// code — see better-auth/plugins/mcp/authorize). MCP clients don't send that,// so a connecting client would be granted a token with no human approval. This// wraps Better Auth's web handler and adds `consent` to the `prompt` of every// `GET /api/auth/mcp/authorize`, so — paired with `oidcConfig.consentPage` —// every connect is gated on the `/oauth/consent` approval screen.//// Pure + Effect-free; the wrapper is a plain Request -> Request transform so it// composes with whatever serves the Better Auth handler (prod + vite dev both// mount the same handler). const AUTHORIZE_PATH = "/api/auth/mcp/authorize";const CONSENT_PAGE = "/mcp-consent"; /** Merge `consent` into a possibly-empty space-separated `prompt` value. */export const promptWithConsent = (prompt: string | null): string => {  const set = new Set((prompt ?? "").split(/\s+/).filter((value) => value.length > 0));  set.add("consent");  return Array.from(set).join(" ");}; /** * Return the MCP-authorize request with `prompt=consent` ensured, or the * original request unchanged when it isn't an MCP authorize call. */export const withForcedMcpConsent = (request: Request): Request => {  if (request.method !== "GET") return request;  const url = new URL(request.url);  if (url.pathname !== AUTHORIZE_PATH) return request;  const prompt = url.searchParams.get("prompt");  if (prompt && prompt.split(/\s+/).includes("consent")) return request;  url.searchParams.set("prompt", promptWithConsent(prompt));  return new Request(url, request);}; /** * If `location` is Better Auth's redirect to the consent page carrying a * `client_id` but no `client_name`, return that client id (so the caller can * look up the registered name and enrich the redirect). Otherwise null. * Better Auth's authorize only puts the opaque `client_id` on the consent * redirect; the registered name makes the approval screen legible. */export const consentRedirectClientId = (location: string | null): string | null => {  if (!location) return null;  const url = new URL(location, "http://host.internal");  if (url.pathname !== CONSENT_PAGE) return null;  if (url.searchParams.get("client_name")) return null;  return url.searchParams.get("client_id");}; /** Append `client_name` to a consent-page redirect URL (path + query only). */export const withClientName = (location: string, clientName: string): string => {  const url = new URL(location, "http://host.internal");  url.searchParams.set("client_name", clientName);  return `${url.pathname}${url.search}`;};