From 9e41891d10ce85a79af116d0e1ff951feea6de35 Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura <79000684+nakasyou@users.noreply.github.com> Date: Tue, 18 Nov 2025 04:41:04 +0900 Subject: [PATCH] fix(oauth-proxy): return multiple Set-Cookie headers instead of a single comma-separated header (#6039) --- .../src/plugins/oauth-proxy/index.ts | 51 +++++++++++++++++-- .../plugins/oauth-proxy/oauth-proxy.test.ts | 51 ++++++++++++++++++- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/packages/better-auth/src/plugins/oauth-proxy/index.ts b/packages/better-auth/src/plugins/oauth-proxy/index.ts index cd9c119598..7ec698693a 100644 --- a/packages/better-auth/src/plugins/oauth-proxy/index.ts +++ b/packages/better-auth/src/plugins/oauth-proxy/index.ts @@ -4,7 +4,7 @@ import { createAuthMiddleware, } from "@better-auth/core/api"; import { env } from "@better-auth/core/env"; -import type { EndpointContext } from "better-call"; +import type { CookieOptions, EndpointContext } from "better-call"; import * as z from "zod"; import { originCheck } from "../../api"; import { parseJSON } from "../../client/parser"; @@ -186,12 +186,53 @@ export const oAuthProxy = (opts?: OAuthProxyOptions | undefined) => { filteredAttrs.push("Secure"); } - return filteredAttrs.length > 0 - ? `${name}=${value}; ${filteredAttrs.join("; ")}` - : `${name}=${value}`; + // Build options + const options: CookieOptions = {}; + for (const attr of filteredAttrs) { + const [attrName, attrValue] = attr.split("="); + if (!attrName) continue; + switch (attrName.toLowerCase()) { + case "path": + options.path = attrValue; + break; + case "expires": + if (!attrValue) break; + options.expires = new Date(attrValue); + break; + case "samesite": + options.sameSite = attrValue as "lax" | "strict" | "none"; + break; + case "httponly": + options.httpOnly = true; + break; + case "max-age": + if (!attrValue) break; + options.maxAge = parseInt(attrValue, 10); + break; + case "prefix": + if (!attrValue) break; + options.prefix = attrValue as "host" | "secure"; + break; + case "partitioned": { + options.partitioned = true; + break; + } + } + } + + return { + name, + value, + options, + }; }); - ctx.setHeader("set-cookie", processedCookies.join(", ")); + for (const cookie of processedCookies) { + // using `ctx.setHeader` overrides previous Set-Cookie headers + // so use ctx.setCookie helper instead + // https://github.com/Bekacru/better-call/blob/d27ac20e64b329a4851e97adf864098a9bc2a260/src/context.ts#L217 + ctx.setCookie(cookie.name, cookie.value, cookie.options); + } throw ctx.redirect(ctx.query.callbackURL); }, ), diff --git a/packages/better-auth/src/plugins/oauth-proxy/oauth-proxy.test.ts b/packages/better-auth/src/plugins/oauth-proxy/oauth-proxy.test.ts index ab493f53a7..bf0cd3e10b 100644 --- a/packages/better-auth/src/plugins/oauth-proxy/oauth-proxy.test.ts +++ b/packages/better-auth/src/plugins/oauth-proxy/oauth-proxy.test.ts @@ -2,7 +2,7 @@ import type { GoogleProfile } from "@better-auth/core/social-providers"; import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { signJWT } from "../../crypto"; +import { signJWT, symmetricEncrypt } from "../../crypto"; import { getTestInstance } from "../../test-utils/test-instance"; import { DEFAULT_SECRET } from "../../utils/constants"; import { oAuthProxy } from "."; @@ -244,4 +244,53 @@ describe("oauth-proxy", async () => { }, }); }); + it("should redirect to proxy url", async () => { + const { client, auth } = await getTestInstance({ + plugins: [ + oAuthProxy({ + currentURL: "http://preview-localhost:3000", + }), + ], + socialProviders: { + google: { + clientId: "test", + clientSecret: "test", + }, + }, + }); + const { secret } = await auth.$context; + + const mockCookies = { + sessionid: "abcd1234", + state: "statevalue", + }; + const mockCookiesString = Object.entries(mockCookies) + .map(([k, v]) => `${k}=${v}`) + .join(", "); + const cookies = await symmetricEncrypt({ + key: secret, + data: mockCookiesString, + }); + + await client.$fetch( + `/oauth-proxy-callback?callbackURL=%2Fdashboard&cookies=${cookies}`, + { + onError(context) { + const headersList = [...context.response.headers]; + const parsedCookies: Record = {}; + for (const [key, value] of headersList) { + if (key.toLowerCase() === "set-cookie") { + const [cookiePair] = value.split(";"); + if (!cookiePair) continue; + const [cookieKey, cookieValue] = cookiePair.split("="); + if (cookieKey === undefined || cookieValue === undefined) + continue; + parsedCookies[cookieKey] = cookieValue; + } + } + expect(mockCookies).toEqual(parsedCookies); + }, + }, + ); + }); });