From 6293e7b41707f046d105b47f29c8eab204828593 Mon Sep 17 00:00:00 2001 From: Sergey Ivochkin Date: Tue, 23 Jun 2026 16:33:17 +1000 Subject: [PATCH] Fix MCP OAuth sign-in deadlock, add Google option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues made the MCP browser sign-in fail repeatedly: 1. In-flight cookie deadlock. /authorize set a `pags_mcp_oauth_inflight` cookie (Max-Age 120s) and short-circuited any new /authorize to a dead-end "already in progress in another tab" page. The cookie was only cleared on a *successful* callback, so any failed or abandoned flow blocked all retries in the same browser for two minutes and never showed a sign-in screen. PKCE + per-request nonce already isolate flows, so the cookie added no security — removed it entirely. 2. GitHub-only sign-in. The consent page hardcoded a single "Continue with GitHub" button. Now it offers both GitHub and Google, matching the rest of the platform. /authorize/continue takes a `provider` param and routes to the matching FAS start endpoint (/v1/auth/{github,google}/start); both already accept the same app_id/return_to/response_mode and return fas_session identically. Defaults to GitHub when provider is omitted. The MCP callback host (mcp.proagentstore.online) is already on the FAS return_to allowlist for both providers, so no FAS-side change is needed. Co-Authored-By: Claude Opus 4.8 (1M context) --- workers/mcp/src/oauth-provider.test.ts | 59 +++++++++++++++++++++++--- workers/mcp/src/oauth-provider.ts | 44 +++++++++---------- 2 files changed, 75 insertions(+), 28 deletions(-) diff --git a/workers/mcp/src/oauth-provider.test.ts b/workers/mcp/src/oauth-provider.test.ts index e2523483..73e34175 100644 --- a/workers/mcp/src/oauth-provider.test.ts +++ b/workers/mcp/src/oauth-provider.test.ts @@ -121,17 +121,19 @@ describe("handleOAuthRoute", () => { ); expect(res?.status).toBe(200); - expect(res?.headers.get("Set-Cookie")).toContain( - "pags_mcp_oauth_inflight=1", - ); if (!res) throw new Error("Expected OAuth response"); + // The in-flight cookie that used to deadlock retries is gone. + expect(res.headers.get("Set-Cookie")).toBeNull(); const html = await res.text(); expect(html).toContain("Connect ProAgentStore MCP"); expect(html).toContain("Codex wants to use ProAgentStore MCP tools"); expect(html).toContain("/authorize/continue?nonce="); + // Both providers are offered. + expect(html).toContain("provider=github"); + expect(html).toContain("provider=google"); }); - it("redirects to FAS OAuth after the user continues", async () => { + it("redirects to FAS GitHub OAuth after the user continues", async () => { const kv = makeKv({ "authreq:nonce-1": JSON.stringify({ clientId: "client-1", @@ -143,7 +145,7 @@ describe("handleOAuthRoute", () => { const res = await handleOAuthRoute( new Request( - "https://mcp.proagentstore.online/authorize/continue?nonce=nonce-1", + "https://mcp.proagentstore.online/authorize/continue?nonce=nonce-1&provider=github", ), config(kv), ); @@ -156,6 +158,53 @@ describe("handleOAuthRoute", () => { expect(res?.headers.get("Location")).toContain("response_mode=query"); }); + it("redirects to FAS Google OAuth when provider=google", async () => { + const kv = makeKv({ + "authreq:nonce-1": JSON.stringify({ + clientId: "client-1", + redirectUri: "http://127.0.0.1:9876/callback", + codeChallenge: "abc", + state: null, + }), + }); + + const res = await handleOAuthRoute( + new Request( + "https://mcp.proagentstore.online/authorize/continue?nonce=nonce-1&provider=google", + ), + config(kv), + ); + + expect(res?.status).toBe(302); + expect(res?.headers.get("Location")).toContain( + "https://api.freeappstore.online/v1/auth/google/start", + ); + expect(res?.headers.get("Location")).toContain("app_id=pags-mcp"); + }); + + it("defaults to GitHub when no provider is given", async () => { + const kv = makeKv({ + "authreq:nonce-1": JSON.stringify({ + clientId: "client-1", + redirectUri: "http://127.0.0.1:9876/callback", + codeChallenge: "abc", + state: null, + }), + }); + + const res = await handleOAuthRoute( + new Request( + "https://mcp.proagentstore.online/authorize/continue?nonce=nonce-1", + ), + config(kv), + ); + + expect(res?.status).toBe(302); + expect(res?.headers.get("Location")).toContain( + "https://api.freeappstore.online/v1/auth/github/start", + ); + }); + it("resolves scoped OAuth access tokens", async () => { const kv = makeKv({ "token:access-1": JSON.stringify({ diff --git a/workers/mcp/src/oauth-provider.ts b/workers/mcp/src/oauth-provider.ts index 59460c34..dfbd795c 100644 --- a/workers/mcp/src/oauth-provider.ts +++ b/workers/mcp/src/oauth-provider.ts @@ -1,6 +1,6 @@ import { MCP_SCOPES, parseScopes } from "./safety.js"; -const AUTH_IN_FLIGHT_COOKIE = "pags_mcp_oauth_inflight"; +type AuthProvider = "github" | "google"; export interface OAuthConfig { issuer: string; @@ -115,15 +115,6 @@ function json(data: unknown, status = 200): Response { }); } -function cookieValue(request: Request, name: string): string | null { - const raw = request.headers.get("Cookie") ?? ""; - for (const part of raw.split(";")) { - const [k, ...v] = part.trim().split("="); - if (k === name) return v.join("=") || ""; - } - return null; -} - function escapeHtml(value: string): string { return value.replace(/[&<>"']/g, (ch) => ({ "&": "&", @@ -134,11 +125,10 @@ function escapeHtml(value: string): string { })[ch] || ch); } -function authAlreadyInProgress(): Response { - return new Response( - "ProAgentStore sign-in

ProAgentStore MCP sign-in is already in progress in another tab. Complete that sign-in, then return to your MCP client.

", - { headers: { "Content-Type": "text/html; charset=utf-8" } }, - ); +function startEndpointFor(config: OAuthConfig, provider: AuthProvider): string { + const base = config.authStart.replace(/\/(?:github|google)\/start$/, ""); + if (base === config.authStart) return config.authStart; + return `${base}/${provider}/start`; } async function register(request: Request, config: OAuthConfig): Promise { @@ -193,7 +183,6 @@ async function authorize(request: Request, config: OAuthConfig): Promise { + const continueUrl = new URL("/authorize/continue", config.issuer); + continueUrl.searchParams.set("nonce", nonce); + continueUrl.searchParams.set("provider", provider); + return escapeHtml(continueUrl.toString()); + }; const clientName = client.client_name ? escapeHtml(client.client_name) : "your MCP client"; return new Response( ` @@ -224,21 +217,26 @@ async function authorize(request: Request, config: OAuthConfig): Promise

Connect ProAgentStore MCP

${clientName} wants to use ProAgentStore MCP tools as your account.

- Continue with GitHub +
`, { headers: { "Content-Type": "text/html; charset=utf-8", - "Set-Cookie": `${AUTH_IN_FLIGHT_COOKIE}=1; Max-Age=120; Path=/; Secure; HttpOnly; SameSite=Lax`, }, }, ); @@ -251,7 +249,8 @@ async function continueAuthorize(request: Request, config: OAuthConfig): Promise const reqRaw = await config.kv.get(`authreq:${nonce}`); if (!reqRaw) return new Response("invalid or expired nonce", { status: 400 }); - const authUrl = new URL(config.authStart); + const provider: AuthProvider = url.searchParams.get("provider") === "google" ? "google" : "github"; + const authUrl = new URL(startEndpointFor(config, provider)); authUrl.searchParams.set("response_mode", "query"); authUrl.searchParams.set("app_id", "pags-mcp"); const callbackUrl = new URL("/oauth/callback", config.issuer); @@ -332,7 +331,6 @@ async function oauthCallback(request: Request, config: OAuthConfig): Promise