From d7c41baa1fdd2f088a6f09b4aea422174bad0acc Mon Sep 17 00:00:00 2001 From: swithek <52840391+swithek@users.noreply.github.com> Date: Mon, 6 Jul 2026 22:04:59 +0200 Subject: [PATCH] fix(electron): forward each Set-Cookie from /electron/init-oauth-proxy individually (#9672) --- ...ix-electron-init-oauth-proxy-set-cookie.md | 5 ++++ packages/electron/src/routes.ts | 21 ++++++++----- packages/electron/test/electron.test.ts | 30 +++++++++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 .changeset/fix-electron-init-oauth-proxy-set-cookie.md diff --git a/.changeset/fix-electron-init-oauth-proxy-set-cookie.md b/.changeset/fix-electron-init-oauth-proxy-set-cookie.md new file mode 100644 index 0000000000..43738f71cc --- /dev/null +++ b/.changeset/fix-electron-init-oauth-proxy-set-cookie.md @@ -0,0 +1,5 @@ +--- +"@better-auth/electron": patch +--- + +`/electron/init-oauth-proxy` now forwards each Set-Cookie from the inner sign-in response separately. The previous `Headers.get("set-cookie")` returned them as one comma-joined string, so the browser silently dropped the transfer-token cookie that the desktop deep-link handoff needs. diff --git a/packages/electron/src/routes.ts b/packages/electron/src/routes.ts index 43eefab48f..f3d2ef3608 100644 --- a/packages/electron/src/routes.ts +++ b/packages/electron/src/routes.ts @@ -8,7 +8,11 @@ import { base64Url } from "@better-auth/utils/base64"; import { createHash } from "@better-auth/utils/hash"; import { betterFetch } from "@better-fetch/fetch"; import { createAuthEndpoint, sessionMiddleware } from "better-auth/api"; -import { setSessionCookie } from "better-auth/cookies"; +import { + parseSetCookieHeader, + setSessionCookie, + toCookieOptions, +} from "better-auth/cookies"; import type { User } from "better-auth/db"; import { parseUserOutput } from "better-auth/db"; import * as z from "zod"; @@ -216,7 +220,7 @@ export const electronInitOAuthProxy = (opts: ElectronOptions) => const headers = new Headers(ctx.request?.headers); headers.set("origin", new URL(ctx.context.baseURL).origin); - let setCookie: string | null = null; + let setCookies: string[] = []; const searchParams = new URLSearchParams(); searchParams.set("client_id", opts.clientID || "electron"); searchParams.set("code_challenge", ctx.query.code_challenge); @@ -235,9 +239,8 @@ export const electronInitOAuthProxy = (opts: ElectronOptions) => body: { provider: ctx.query.provider, }, - onResponse: (ctx) => { - const headers = ctx.response.headers; - setCookie = headers.get("set-cookie") ?? null; + onResponse: (innerCtx) => { + setCookies = innerCtx.response.headers.getSetCookie(); }, headers, }, @@ -249,9 +252,13 @@ export const electronInitOAuthProxy = (opts: ElectronOptions) => }); } - if (setCookie) { - ctx.setHeader("set-cookie", setCookie); + for (const cookieStr of setCookies) { + const parsed = parseSetCookieHeader(cookieStr); + parsed.forEach((attrs, name) => { + ctx.setCookie(name, attrs.value, toCookieOptions(attrs)); + }); } + if (res.data.url && res.data.redirect) { ctx.setHeader("Location", res.data.url); ctx.setStatus(302); diff --git a/packages/electron/test/electron.test.ts b/packages/electron/test/electron.test.ts index 7860fcb008..54a6e91508 100644 --- a/packages/electron/test/electron.test.ts +++ b/packages/electron/test/electron.test.ts @@ -179,6 +179,36 @@ describe("Electron", () => { expect(data!.electron_authorization_code).toBeTypeOf("string"); }); + it("/electron/init-oauth-proxy should forward each Set-Cookie from the inner sign-in response as a separate header", async () => { + const innerHeaders = new Headers(); + innerHeaders.append("set-cookie", "first.cookie=one; Path=/; HttpOnly"); + innerHeaders.append("set-cookie", "second.cookie=two; Path=/; HttpOnly"); + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response( + JSON.stringify({ + url: "https://provider.example/oauth/authorize", + redirect: true, + }), + { status: 200, headers: innerHeaders }, + ), + ); + + const res = (await (auth.api as any).electronInitOAuthProxy({ + query: { + provider: "google", + state: "x", + code_challenge: "y", + code_challenge_method: "S256", + }, + asResponse: true, + })) as Response; + + const setCookies = res.headers.getSetCookie(); + expect(setCookies).toHaveLength(2); + expect(setCookies[0]).toMatch(/^first\.cookie=one/); + expect(setCookies[1]).toMatch(/^second\.cookie=two/); + }); + it("should exchange token", async ({ setProcessType }) => { setProcessType("browser");