fix(core): keep MCP OAuth login waiting after stray callback requests (#58342)

Co-authored-by: Reynold Hu <88558425+reynold-hu@users.noreply.github.com>
This commit is contained in:
Aiden Cline 2026-10-22 23:37:08 -05:00 • committed by GitHub
parent e49157b34c
commit 309522f0ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 41 additions and 1 deletions

View file

@ -373,8 +373,17 @@ export const authorize = (input: {
}
const error = url.searchParams.get("error_description") ?? url.searchParams.get("error")
if (error) return fail(error, "authorization_error")
if (url.searchParams.get("state") !== state) return fail("OAuth state mismatch", "state_mismatch")
const returned = url.searchParams.get("state")
const value = url.searchParams.get("code")
// Browsers can reach the callback without any OAuth parameters (preflights, prefetches, reloads). Only a
// redirect from the authorization server may end the login, so keep waiting for it.
if (returned === null && !value) {
response
.writeHead(400, { "Content-Type": "text/html" })
.end(OauthCallbackPage.error("Missing authorization code", { provider: input.name }))
return
}
if (returned !== state) return fail("OAuth state mismatch", "state_mismatch")
if (!value) return fail("Missing authorization code", "missing_code")
// The page waits for the token exchange so the browser never reports success for a rejected code.
if (

View file

@ -160,6 +160,37 @@ describe("MCP OAuth", () => {
expect(tokenRequests[0]?.get("resource")).toBe(server.url.href)
})
test("keeps waiting for the redirect after callback requests without OAuth parameters", async () => {
const server = Bun.serve({
port: 0,
async fetch(request) {
if (request.method !== "POST" || new URL(request.url).pathname !== "/token")
return new Response(null, { status: 404 })
return Response.json({ access_token: "access", token_type: "Bearer", refresh_token: "refresh", expires_in: 3600 })
},
})
const credential = await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const { authorization, url: authorizationUrl } = yield* start(server, { client_id: "client" })
const redirectValue = authorizationUrl.searchParams.get("redirect_uri")
const state = authorizationUrl.searchParams.get("state")
if (!redirectValue || !state) throw new Error("Missing OAuth redirect parameters")
const callback = new URL(redirectValue)
expect((yield* Effect.promise(() => fetch(callback, { method: "OPTIONS" }))).status).toBe(400)
expect((yield* Effect.promise(() => fetch(callback))).status).toBe(400)
callback.searchParams.set("code", "accepted")
callback.searchParams.set("state", state)
expect((yield* Effect.promise(() => fetch(callback))).status).toBe(200)
return yield* authorization.callback
}),
),
).finally(() => server.stop(true))
expect(credential.access).toBe("access")
})
test("reports a rejected token exchange on the callback page", async () => {
const server = Bun.serve({
port: 0,