diff --git a/packages/mcp/src/oauth.ts b/packages/mcp/src/oauth.ts index 64073bd..35c1b89 100644 --- a/packages/mcp/src/oauth.ts +++ b/packages/mcp/src/oauth.ts @@ -96,8 +96,12 @@ function delegatedFetch(config: OAuthConfig, subject: string, expiresAt: number, .sign(config.secret); headers.set("X-NLB-MCP-Assertion", assertion); } - // Never forward credentials across an upstream redirect. - return fetch(new Request(request, { headers, redirect: "error" })); + // Workers supports only follow/manual. Reject redirects before any second request. + const response = await fetch(new Request(request, { headers, redirect: "manual" })); + if (response.status >= 300 && response.status < 400) { + throw new Error("OAuth API redirects are not allowed"); + } + return response; }; } diff --git a/packages/mcp/tests/oauth.test.ts b/packages/mcp/tests/oauth.test.ts index a80566e..80cc621 100644 --- a/packages/mcp/tests/oauth.test.ts +++ b/packages/mcp/tests/oauth.test.ts @@ -164,12 +164,19 @@ describe("MCP OAuth resource server", () => { expect(upstreamRequests).toHaveLength(0); }); - it("does not follow authenticated redirects or sign requests for another API", async () => { + it("uses Workers-supported manual redirects and rejects redirects or another API", async () => { const context = await authorizeOAuthMessage(new Request(resource, { headers: { Authorization: `Bearer ${await token()}` } }), { method: "tools/call", params: { name: "submit_product" } }, env, { workerAuth: false }); if (context instanceof Response || !context.fetch) throw new Error("Expected authenticated context"); upstreamRequests = []; - await expect(context.fetch(`${env.NLB_API_URL}/api/redirect`)).rejects.toThrow(); + const fetchSpy = vi.spyOn(globalThis, "fetch"); + try { + await expect(context.fetch(`${env.NLB_API_URL}/api/redirect`)).rejects.toThrow("OAuth API redirects are not allowed"); + // Node accepts redirect:error, but the deployed Workers runtime rejects that mode. + expect((fetchSpy.mock.calls[0][0] as Request).redirect).toBe("manual"); + } finally { + fetchSpy.mockRestore(); + } expect(upstreamRequests.map((r) => r.path)).toEqual(["/api/redirect"]); await expect(context.fetch("https://untrusted.example/api/private")).rejects.toThrow("configured NLB API"); });