fix(electron): forward each Set-Cookie from /electron/init-oauth-proxy individually (#9672)

This commit is contained in:
swithek
2026-07-06 20:04:59 +00:00
committed by GitHub
parent 8954563112
commit d7c41baa1f
3 changed files with 49 additions and 7 deletions
@@ -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.
+14 -7
View File
@@ -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);
+30
View File
@@ -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");