From 06616c20d9b06e6aaeb583a5f4451e504ed0c559 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Mon, 20 Oct 2025 23:00:05 +0530 Subject: [PATCH 1/3] fix: use store and retrieve authRequest APIs --- packages/mcp-common/package.json | 2 +- packages/mcp-common/src/cloudflare-auth.ts | 4 +- .../src/cloudflare-oauth-handler.ts | 36 +++++------ pnpm-lock.yaml | 63 ++++++++++--------- 4 files changed, 52 insertions(+), 53 deletions(-) diff --git a/packages/mcp-common/package.json b/packages/mcp-common/package.json index e1b526e7..43794f40 100644 --- a/packages/mcp-common/package.json +++ b/packages/mcp-common/package.json @@ -11,7 +11,7 @@ "test:coverage": "run-vitest-coverage" }, "dependencies": { - "@cloudflare/workers-oauth-provider": "0.0.5", + "@cloudflare/workers-oauth-provider": "0.0.12", "@fast-csv/format": "5.0.2", "@hono/zod-validator": "0.4.3", "@modelcontextprotocol/sdk": "1.18.2", diff --git a/packages/mcp-common/src/cloudflare-auth.ts b/packages/mcp-common/src/cloudflare-auth.ts index e77c798d..b389e5f5 100644 --- a/packages/mcp-common/src/cloudflare-auth.ts +++ b/packages/mcp-common/src/cloudflare-auth.ts @@ -83,7 +83,7 @@ export async function getAuthorizationURL({ }: { client_id: string redirect_uri: string - state: AuthRequest + state: string scopes: Record }): Promise<{ authUrl: string; codeVerifier: string }> { const { codeChallenge, codeVerifier } = await generatePKCECodes() @@ -92,7 +92,7 @@ export async function getAuthorizationURL({ authUrl: generateAuthUrl({ client_id, redirect_uri, - state: btoa(JSON.stringify({ ...state, codeVerifier })), + state, code_challenge: codeChallenge, scopes, }), diff --git a/packages/mcp-common/src/cloudflare-oauth-handler.ts b/packages/mcp-common/src/cloudflare-oauth-handler.ts index 339e45ad..213cae32 100644 --- a/packages/mcp-common/src/cloudflare-oauth-handler.ts +++ b/packages/mcp-common/src/cloudflare-oauth-handler.ts @@ -25,21 +25,6 @@ type AuthContext = { } } & BaseHonoContext -const AuthRequestSchema = z.object({ - responseType: z.string(), - clientId: z.string(), - redirectUri: z.string(), - scope: z.array(z.string()), - state: z.string(), - codeChallenge: z.string().optional(), - codeChallengeMethod: z.string().optional(), -}) - -// AuthRequest but with extra params that we use in our authentication logic -const AuthRequestSchemaWithExtraParams = AuthRequestSchema.merge( - z.object({ codeVerifier: z.string() }) -) - const AuthQuery = z.object({ code: z.string().describe('OAuth code from CF dash'), state: z.string().describe('Value of the OAuth state'), @@ -220,14 +205,23 @@ export function createAuthHandlers({ if (!oauthReqInfo.clientId) { return c.text('Invalid request', 400) } - const res = await getAuthorizationURL({ + + // Generate UUID to prevent CSRF attacks + const authRequestId = crypto.randomUUID() + const { authUrl, codeVerifier } = await getAuthorizationURL({ client_id: c.env.CLOUDFLARE_CLIENT_ID, redirect_uri: new URL('/oauth/callback', c.req.url).href, - state: oauthReqInfo, + state: authRequestId, scopes, }) - return Response.redirect(res.authUrl, 302) + // Store auth request information in KV + c.env.OAUTH_PROVIDER.storeAuthRequest(authRequestId, { + ...oauthReqInfo, + codeVerifier, + }) + + return Response.redirect(authUrl, 302) } catch (e) { c.var.sentry?.recordError(e) let message: string | undefined @@ -262,9 +256,9 @@ export function createAuthHandlers({ app.get(`/oauth/callback`, zValidator('query', AuthQuery), async (c) => { try { const { state, code } = c.req.valid('query') - const oauthReqInfo = AuthRequestSchemaWithExtraParams.parse(JSON.parse(atob(state))) - // Get the oathReqInfo out of KV - if (!oauthReqInfo.clientId) { + const oauthReqInfo = await c.env.OAUTH_PROVIDER.getAuthRequest(state) + + if (!oauthReqInfo || !oauthReqInfo.clientId) { throw new McpError('Invalid State', 400) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca444ca8..c41e4bde 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1026,8 +1026,8 @@ importers: packages/mcp-common: dependencies: '@cloudflare/workers-oauth-provider': - specifier: 0.0.5 - version: 0.0.5 + specifier: 0.0.12 + version: 0.0.12 '@fast-csv/format': specifier: 5.0.2 version: 5.0.2 @@ -1338,11 +1338,11 @@ packages: resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} '@cloudflare/kv-asset-handler@0.4.0': - resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==, tarball: https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz} + resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} engines: {node: '>=18.0.0'} '@cloudflare/unenv-preset@2.3.1': - resolution: {integrity: sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==, tarball: https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.1.tgz} + resolution: {integrity: sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==} peerDependencies: unenv: 2.0.0-rc.15 workerd: ^1.20250320.0 @@ -1351,146 +1351,149 @@ packages: optional: true '@cloudflare/vite-plugin@1.1.0': - resolution: {integrity: sha512-b265RnBqZE57KBPPwhDWFu8W51RNnl4LkxNgY/GzbXoztc6qDcnMs7IVyPCcCvyXa4ogSQz5MvQ3yB5Ehn5E8A==, tarball: https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.1.0.tgz} + resolution: {integrity: sha512-b265RnBqZE57KBPPwhDWFu8W51RNnl4LkxNgY/GzbXoztc6qDcnMs7IVyPCcCvyXa4ogSQz5MvQ3yB5Ehn5E8A==} peerDependencies: vite: ^6.1.0 wrangler: ^3.101.0 || ^4.0.0 '@cloudflare/vitest-pool-workers@0.8.14': - resolution: {integrity: sha512-uqUvelQQkU/8JD/mgd9OV3byB6CaHxkw/DzHZ4z2haM9epR9D5mDshw5+AfPu7x/IWZ9zciaDawjM8QW8QrjIA==, tarball: https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.8.14.tgz} + resolution: {integrity: sha512-uqUvelQQkU/8JD/mgd9OV3byB6CaHxkw/DzHZ4z2haM9epR9D5mDshw5+AfPu7x/IWZ9zciaDawjM8QW8QrjIA==} peerDependencies: '@vitest/runner': 2.0.x - 3.0.x '@vitest/snapshot': 2.0.x - 3.0.x vitest: 2.0.x - 3.0.x '@cloudflare/workerd-darwin-64@1.20250408.0': - resolution: {integrity: sha512-bxhIwBWxaNItZLXDNOKY2dCv0FHjDiDkfJFpwv4HvtvU5MKcrivZHVmmfDzLW85rqzfcDOmKbZeMPVfiKxdBZw==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250408.0.tgz} + resolution: {integrity: sha512-bxhIwBWxaNItZLXDNOKY2dCv0FHjDiDkfJFpwv4HvtvU5MKcrivZHVmmfDzLW85rqzfcDOmKbZeMPVfiKxdBZw==} engines: {node: '>=16'} cpu: [x64] os: [darwin] '@cloudflare/workerd-darwin-64@1.20250409.0': - resolution: {integrity: sha512-smA9yq77xsdQ1NMLhFz3JZxMHGd01lg0bE+X3dTFmIUs+hHskJ+HJ/IkMFInkCCeEFlUkoL4yO7ilaU/fin/xA==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250409.0.tgz} + resolution: {integrity: sha512-smA9yq77xsdQ1NMLhFz3JZxMHGd01lg0bE+X3dTFmIUs+hHskJ+HJ/IkMFInkCCeEFlUkoL4yO7ilaU/fin/xA==} engines: {node: '>=16'} cpu: [x64] os: [darwin] '@cloudflare/workerd-darwin-64@1.20250428.0': - resolution: {integrity: sha512-6nVe9oV4Hdec6ctzMtW80TiDvNTd2oFPi3VsKqSDVaJSJbL+4b6seyJ7G/UEPI+si6JhHBSLV2/9lNXNGLjClA==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250428.0.tgz} + resolution: {integrity: sha512-6nVe9oV4Hdec6ctzMtW80TiDvNTd2oFPi3VsKqSDVaJSJbL+4b6seyJ7G/UEPI+si6JhHBSLV2/9lNXNGLjClA==} engines: {node: '>=16'} cpu: [x64] os: [darwin] '@cloudflare/workerd-darwin-64@1.20250507.0': - resolution: {integrity: sha512-xC+8hmQuOUUNCVT9DWpLMfxhR4Xs4kI8v7Bkybh4pzGC85moH6fMfCBNaP0YQCNAA/BR56aL/AwfvMVGskTK/A==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250507.0.tgz} + resolution: {integrity: sha512-xC+8hmQuOUUNCVT9DWpLMfxhR4Xs4kI8v7Bkybh4pzGC85moH6fMfCBNaP0YQCNAA/BR56aL/AwfvMVGskTK/A==} engines: {node: '>=16'} cpu: [x64] os: [darwin] '@cloudflare/workerd-darwin-arm64@1.20250408.0': - resolution: {integrity: sha512-5XZ2Oykr8bSo7zBmERtHh18h5BZYC/6H1YFWVxEj3PtalF3+6SHsO4KZsbGvDml9Pu7sHV277jiZE5eny8Hlyw==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250408.0.tgz} + resolution: {integrity: sha512-5XZ2Oykr8bSo7zBmERtHh18h5BZYC/6H1YFWVxEj3PtalF3+6SHsO4KZsbGvDml9Pu7sHV277jiZE5eny8Hlyw==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] '@cloudflare/workerd-darwin-arm64@1.20250409.0': - resolution: {integrity: sha512-oLVcf+Y5Qun8JHcy1VcR/YnbA5U2ne0czh3XNhDqdHZFK8+vKeC7MnVPX+kEqQA3+uLcMM1/FsIDU1U4Na0h1g==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250409.0.tgz} + resolution: {integrity: sha512-oLVcf+Y5Qun8JHcy1VcR/YnbA5U2ne0czh3XNhDqdHZFK8+vKeC7MnVPX+kEqQA3+uLcMM1/FsIDU1U4Na0h1g==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] '@cloudflare/workerd-darwin-arm64@1.20250428.0': - resolution: {integrity: sha512-/TB7bh7SIJ5f+6r4PHsAz7+9Qal/TK1cJuKFkUno1kqGlZbdrMwH0ATYwlWC/nBFeu2FB3NUolsTntEuy23hnQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250428.0.tgz} + resolution: {integrity: sha512-/TB7bh7SIJ5f+6r4PHsAz7+9Qal/TK1cJuKFkUno1kqGlZbdrMwH0ATYwlWC/nBFeu2FB3NUolsTntEuy23hnQ==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] '@cloudflare/workerd-darwin-arm64@1.20250507.0': - resolution: {integrity: sha512-Oynff5H8yM4trfUFaKdkOvPV3jac8mg7QC19ILZluCVgLx/JGEVLEJ7do1Na9rLqV8CK4gmUXPrUMX7uerhQgg==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250507.0.tgz} + resolution: {integrity: sha512-Oynff5H8yM4trfUFaKdkOvPV3jac8mg7QC19ILZluCVgLx/JGEVLEJ7do1Na9rLqV8CK4gmUXPrUMX7uerhQgg==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] '@cloudflare/workerd-linux-64@1.20250408.0': - resolution: {integrity: sha512-WbgItXWln6G5d7GvYLWcuOzAVwafysZaWunH3UEfsm95wPuRofpYnlDD861gdWJX10IHSVgMStGESUcs7FLerQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250408.0.tgz} + resolution: {integrity: sha512-WbgItXWln6G5d7GvYLWcuOzAVwafysZaWunH3UEfsm95wPuRofpYnlDD861gdWJX10IHSVgMStGESUcs7FLerQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] '@cloudflare/workerd-linux-64@1.20250409.0': - resolution: {integrity: sha512-D31B4kdC3a0RD5yfpdIa89//kGHbYsYihZmejm1k4S4NHOho3MUDHAEh4aHtafQNXbZdydGHlSyiVYjTdQ9ILQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250409.0.tgz} + resolution: {integrity: sha512-D31B4kdC3a0RD5yfpdIa89//kGHbYsYihZmejm1k4S4NHOho3MUDHAEh4aHtafQNXbZdydGHlSyiVYjTdQ9ILQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] '@cloudflare/workerd-linux-64@1.20250428.0': - resolution: {integrity: sha512-9eCbj+R3CKqpiXP6DfAA20DxKge+OTj7Hyw3ZewiEhWH9INIHiJwJQYybu4iq9kJEGjnGvxgguLFjSCWm26hgg==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250428.0.tgz} + resolution: {integrity: sha512-9eCbj+R3CKqpiXP6DfAA20DxKge+OTj7Hyw3ZewiEhWH9INIHiJwJQYybu4iq9kJEGjnGvxgguLFjSCWm26hgg==} engines: {node: '>=16'} cpu: [x64] os: [linux] '@cloudflare/workerd-linux-64@1.20250507.0': - resolution: {integrity: sha512-/HAA+Zg/R7Q/Smyl835FUFKjotZN1UzN9j/BHBd0xKmKov97QkXAX8gsyGnyKqRReIOinp8x/8+UebTICR7VJw==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250507.0.tgz} + resolution: {integrity: sha512-/HAA+Zg/R7Q/Smyl835FUFKjotZN1UzN9j/BHBd0xKmKov97QkXAX8gsyGnyKqRReIOinp8x/8+UebTICR7VJw==} engines: {node: '>=16'} cpu: [x64] os: [linux] '@cloudflare/workerd-linux-arm64@1.20250408.0': - resolution: {integrity: sha512-pAhEywPPvr92SLylnQfZEPgXz+9pOG9G9haAPLpEatncZwYiYd9yiR6HYWhKp2erzCoNrOqKg9IlQwU3z1IDiw==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250408.0.tgz} + resolution: {integrity: sha512-pAhEywPPvr92SLylnQfZEPgXz+9pOG9G9haAPLpEatncZwYiYd9yiR6HYWhKp2erzCoNrOqKg9IlQwU3z1IDiw==} engines: {node: '>=16'} cpu: [arm64] os: [linux] '@cloudflare/workerd-linux-arm64@1.20250409.0': - resolution: {integrity: sha512-Sr59P0TREayil5OQ7kcbjuIn6L6OTSRLI91LKu0D8vi1hss2q9FUwBcwxg0+Yd/x+ty/x7IISiAK5QBkAMeITQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250409.0.tgz} + resolution: {integrity: sha512-Sr59P0TREayil5OQ7kcbjuIn6L6OTSRLI91LKu0D8vi1hss2q9FUwBcwxg0+Yd/x+ty/x7IISiAK5QBkAMeITQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] '@cloudflare/workerd-linux-arm64@1.20250428.0': - resolution: {integrity: sha512-D9NRBnW46nl1EQsP13qfkYb5lbt4C6nxl38SBKY/NOcZAUoHzNB5K0GaK8LxvpkM7X/97ySojlMfR5jh5DNXYQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250428.0.tgz} + resolution: {integrity: sha512-D9NRBnW46nl1EQsP13qfkYb5lbt4C6nxl38SBKY/NOcZAUoHzNB5K0GaK8LxvpkM7X/97ySojlMfR5jh5DNXYQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] '@cloudflare/workerd-linux-arm64@1.20250507.0': - resolution: {integrity: sha512-NMPibSdOYeycU0IrKkgOESFJQy7dEpHvuatZxQxlT+mIQK0INzI3irp2kKxhF99s25kPC4p+xg9bU3ugTrs3VQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250507.0.tgz} + resolution: {integrity: sha512-NMPibSdOYeycU0IrKkgOESFJQy7dEpHvuatZxQxlT+mIQK0INzI3irp2kKxhF99s25kPC4p+xg9bU3ugTrs3VQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] '@cloudflare/workerd-windows-64@1.20250408.0': - resolution: {integrity: sha512-nJ3RjMKGae2aF2rZ/CNeBvQPM+W5V1SUK0FYWG/uomyr7uQ2l4IayHna1ODg/OHHTEgIjwom0Mbn58iXb0WOcQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250408.0.tgz} + resolution: {integrity: sha512-nJ3RjMKGae2aF2rZ/CNeBvQPM+W5V1SUK0FYWG/uomyr7uQ2l4IayHna1ODg/OHHTEgIjwom0Mbn58iXb0WOcQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] '@cloudflare/workerd-windows-64@1.20250409.0': - resolution: {integrity: sha512-dK9I8zBX5rR7MtaaP2AhICQTEw3PVzHcsltN8o46w7JsbYlMvFOj27FfYH5dhs3IahgmIfw2e572QXW2o/dbpg==, tarball: https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250409.0.tgz} + resolution: {integrity: sha512-dK9I8zBX5rR7MtaaP2AhICQTEw3PVzHcsltN8o46w7JsbYlMvFOj27FfYH5dhs3IahgmIfw2e572QXW2o/dbpg==} engines: {node: '>=16'} cpu: [x64] os: [win32] '@cloudflare/workerd-windows-64@1.20250428.0': - resolution: {integrity: sha512-RQCRj28eitjKD0tmei6iFOuWqMuHMHdNGEigRmbkmuTlpbWHNAoHikgCzZQ/dkKDdatA76TmcpbyECNf31oaTA==, tarball: https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250428.0.tgz} + resolution: {integrity: sha512-RQCRj28eitjKD0tmei6iFOuWqMuHMHdNGEigRmbkmuTlpbWHNAoHikgCzZQ/dkKDdatA76TmcpbyECNf31oaTA==} engines: {node: '>=16'} cpu: [x64] os: [win32] '@cloudflare/workerd-windows-64@1.20250507.0': - resolution: {integrity: sha512-c91fhNP8ufycdIDqjVyKTqeb4ewkbAYXFQbLreMVgh4LLQQPDDEte8wCdmaFy5bIL0M9d85PpdCq51RCzq/FaQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250507.0.tgz} + resolution: {integrity: sha512-c91fhNP8ufycdIDqjVyKTqeb4ewkbAYXFQbLreMVgh4LLQQPDDEte8wCdmaFy5bIL0M9d85PpdCq51RCzq/FaQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] + '@cloudflare/workers-oauth-provider@0.0.12': + resolution: {integrity: sha512-A/qTfCFusZBUcb/D62y610p9IcuZ3TBwmP1yYOUstroUIWfx1q0Y8NmbFN7++4ZWTHzVnFoyU6/iTIrC0lDqFA==} + '@cloudflare/workers-oauth-provider@0.0.5': - resolution: {integrity: sha512-t1x5KAzsubCvb4APnJ93z407X1x7SGj/ga5ziRnwIb/iLy4PMkT/hgd1y5z7Bbsdy5Fy6mywhCP4lym24bX66w==, tarball: https://registry.npmjs.org/@cloudflare/workers-oauth-provider/-/workers-oauth-provider-0.0.5.tgz} + resolution: {integrity: sha512-t1x5KAzsubCvb4APnJ93z407X1x7SGj/ga5ziRnwIb/iLy4PMkT/hgd1y5z7Bbsdy5Fy6mywhCP4lym24bX66w==} '@cloudflare/workers-types@4.20250410.0': - resolution: {integrity: sha512-Yx9VUi6QpmXtUIhOL+em+V02gue12kmVBVL6RGH5mhFh50M0x9JyOmm6wKwKZUny2uQd+22nuouE2q3z1OrsIQ==, tarball: https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250410.0.tgz} + resolution: {integrity: sha512-Yx9VUi6QpmXtUIhOL+em+V02gue12kmVBVL6RGH5mhFh50M0x9JyOmm6wKwKZUny2uQd+22nuouE2q3z1OrsIQ==} '@cloudflare/workers-types@4.20250416.0': - resolution: {integrity: sha512-i37TX0Clp+MrPdXMBdvKZM7JghCrWD9GtG7E+8ANOAPmtZjUkZfEy9qq46IG3XlNpagPaWDkY3SgJ3s01gPxCw==, tarball: https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250416.0.tgz} + resolution: {integrity: sha512-i37TX0Clp+MrPdXMBdvKZM7JghCrWD9GtG7E+8ANOAPmtZjUkZfEy9qq46IG3XlNpagPaWDkY3SgJ3s01gPxCw==} '@coinbase/wallet-sdk@3.9.3': resolution: {integrity: sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==} @@ -6543,6 +6546,8 @@ snapshots: '@cloudflare/workerd-windows-64@1.20250507.0': optional: true + '@cloudflare/workers-oauth-provider@0.0.12': {} + '@cloudflare/workers-oauth-provider@0.0.5': dependencies: '@cloudflare/workers-types': 4.20250416.0 From 56ccaa2630230aef9a124c0e57cfc424698b666d Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Wed, 22 Oct 2025 21:58:29 +0530 Subject: [PATCH 2/3] fix: store pkce code_verifier in browser's cookie --- .../src/cloudflare-oauth-handler.ts | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/mcp-common/src/cloudflare-oauth-handler.ts b/packages/mcp-common/src/cloudflare-oauth-handler.ts index 213cae32..a37ad2ec 100644 --- a/packages/mcp-common/src/cloudflare-oauth-handler.ts +++ b/packages/mcp-common/src/cloudflare-oauth-handler.ts @@ -1,5 +1,6 @@ import { zValidator } from '@hono/zod-validator' import { Hono } from 'hono' +import { deleteCookie, getCookie, setCookie } from 'hono/cookie' import { z } from 'zod' import { AuthUser } from '../../mcp-observability/src' @@ -216,12 +217,17 @@ export function createAuthHandlers({ }) // Store auth request information in KV - c.env.OAUTH_PROVIDER.storeAuthRequest(authRequestId, { - ...oauthReqInfo, - codeVerifier, + c.env.OAUTH_PROVIDER.storeAuthRequest(authRequestId, oauthReqInfo) + + // Store the code verifier in a http-only cookie for session fixation protection + setCookie(c, 'cloudflare_pkce_code_verifier', codeVerifier, { + path: '/', + secure: true, + httpOnly: true, + sameSite: 'Lax', }) - return Response.redirect(authUrl, 302) + return c.redirect(authUrl, 302) } catch (e) { c.var.sentry?.recordError(e) let message: string | undefined @@ -256,14 +262,24 @@ export function createAuthHandlers({ app.get(`/oauth/callback`, zValidator('query', AuthQuery), async (c) => { try { const { state, code } = c.req.valid('query') + + // Read AuthRequest and delete immediately const oauthReqInfo = await c.env.OAUTH_PROVIDER.getAuthRequest(state) + await c.env.OAUTH_PROVIDER.deleteAuthRequest(state) + + // Read code verifier from cookie + const codeVerifier = getCookie(c, 'cloudflare_pkce_code_verifier') if (!oauthReqInfo || !oauthReqInfo.clientId) { throw new McpError('Invalid State', 400) } + if (!codeVerifier) { + throw new McpError('Missing PKCE code verifier', 400) + } + const [{ accessToken, refreshToken, user, accounts }] = await Promise.all([ - getTokenAndUserDetails(c, code, oauthReqInfo.codeVerifier), + getTokenAndUserDetails(c, code, codeVerifier), c.env.OAUTH_PROVIDER.createClient({ clientId: oauthReqInfo.clientId, tokenEndpointAuthMethod: 'none', @@ -304,7 +320,9 @@ export function createAuthHandlers({ }) ) - return Response.redirect(redirectTo, 302) + // Clear the cookie on success + deleteCookie(c, 'cloudflare_pkce_code_verifier', { path: '/' }) + return c.redirect(redirectTo, 302) } catch (e) { c.var.sentry?.recordError(e) let message: string | undefined @@ -321,6 +339,8 @@ export function createAuthHandlers({ errorMessage: `Callback Error: ${message}`, }) ) + // Clear the cookie on error + deleteCookie(c, 'cloudflare_pkce_code_verifier', { path: '/' }) if (e instanceof McpError) { return c.text(e.message, { status: e.code }) } From 0a06bc9e9ff7a3f04d45f29b5f4e5dc07222875c Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Thu, 23 Oct 2025 11:59:49 +0530 Subject: [PATCH 3/3] fix: preserve state received from client in /authorize --- .../src/cloudflare-oauth-handler.ts | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/mcp-common/src/cloudflare-oauth-handler.ts b/packages/mcp-common/src/cloudflare-oauth-handler.ts index a37ad2ec..8051ef0e 100644 --- a/packages/mcp-common/src/cloudflare-oauth-handler.ts +++ b/packages/mcp-common/src/cloudflare-oauth-handler.ts @@ -10,6 +10,7 @@ import { useSentry } from './sentry' import { V4Schema } from './v4-api' import type { + AuthRequest, OAuthHelpers, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, @@ -207,20 +208,20 @@ export function createAuthHandlers({ return c.text('Invalid request', 400) } - // Generate UUID to prevent CSRF attacks - const authRequestId = crypto.randomUUID() + if (!oauthReqInfo.state) { + return c.text('Invalid request, missing state', 400) + } + const { authUrl, codeVerifier } = await getAuthorizationURL({ client_id: c.env.CLOUDFLARE_CLIENT_ID, redirect_uri: new URL('/oauth/callback', c.req.url).href, - state: authRequestId, + state: oauthReqInfo.state, scopes, }) - // Store auth request information in KV - c.env.OAUTH_PROVIDER.storeAuthRequest(authRequestId, oauthReqInfo) - - // Store the code verifier in a http-only cookie for session fixation protection - setCookie(c, 'cloudflare_pkce_code_verifier', codeVerifier, { + // Store the entire auth request and code verifier in a secure, http-only cookie + const cookiePayload = JSON.stringify({ ...oauthReqInfo, codeVerifier }) + setCookie(c, 'cloudflare_oauth_request', cookiePayload, { path: '/', secure: true, httpOnly: true, @@ -255,27 +256,31 @@ export function createAuthHandlers({ * OAuth Callback Endpoint * * This route handles the callback from Cloudflare after user authentication. - * It exchanges the temporary code for an access token, then stores some - * user metadata & the auth token as part of the 'props' on the token passed + * It reads the AuthRequest object from the cookie, validates the state for CSRF protection, + * and then uses the code_verifier to exchange the temporary code for an access token. + * It then stores some user metadata & the auth token as part of the 'props' on the token passed * down to the client. It ends by redirecting the client back to _its_ callback URL */ app.get(`/oauth/callback`, zValidator('query', AuthQuery), async (c) => { try { const { state, code } = c.req.valid('query') + const cookiePayload = getCookie(c, 'cloudflare_oauth_request') - // Read AuthRequest and delete immediately - const oauthReqInfo = await c.env.OAUTH_PROVIDER.getAuthRequest(state) - await c.env.OAUTH_PROVIDER.deleteAuthRequest(state) + if (!cookiePayload) { + throw new McpError('Missing auth request cookie', 400) + } - // Read code verifier from cookie - const codeVerifier = getCookie(c, 'cloudflare_pkce_code_verifier') + const { codeVerifier, ...oauthReqInfo } = JSON.parse(cookiePayload) as AuthRequest & { + codeVerifier: string + } - if (!oauthReqInfo || !oauthReqInfo.clientId) { + // Validate the state to prevent CSRF attacks + if (!oauthReqInfo.state || oauthReqInfo.state !== state) { throw new McpError('Invalid State', 400) } if (!codeVerifier) { - throw new McpError('Missing PKCE code verifier', 400) + throw new McpError('Missing PKCE code verifier in cookie', 400) } const [{ accessToken, refreshToken, user, accounts }] = await Promise.all([ @@ -321,7 +326,7 @@ export function createAuthHandlers({ ) // Clear the cookie on success - deleteCookie(c, 'cloudflare_pkce_code_verifier', { path: '/' }) + deleteCookie(c, 'cloudflare_oauth_request', { path: '/' }) return c.redirect(redirectTo, 302) } catch (e) { c.var.sentry?.recordError(e) @@ -340,7 +345,7 @@ export function createAuthHandlers({ }) ) // Clear the cookie on error - deleteCookie(c, 'cloudflare_pkce_code_verifier', { path: '/' }) + deleteCookie(c, 'cloudflare_oauth_request', { path: '/' }) if (e instanceof McpError) { return c.text(e.message, { status: e.code }) }