diff --git a/packages/better-auth/src/cookies/cookie-utils.ts b/packages/better-auth/src/cookies/cookie-utils.ts index e0feba6e93..96ada13fda 100644 --- a/packages/better-auth/src/cookies/cookie-utils.ts +++ b/packages/better-auth/src/cookies/cookie-utils.ts @@ -26,11 +26,55 @@ export function stripSecureCookiePrefix(cookieName: string): string { return cookieName; } +/** + * Split `Set-Cookie` header, handling commas in `Expires` dates. + */ +export function splitSetCookieHeader(setCookie: string): string[] { + if (!setCookie) return []; + + const result: string[] = []; + let current = ""; + let i = 0; + + while (i < setCookie.length) { + const c = setCookie[i]; + + if (c === ",") { + const lower = current.toLowerCase(); + if (lower.includes("expires=") && !lower.includes("gmt")) { + current += c; + i++; + } else { + const trimmed = current.trim(); + if (trimmed) { + result.push(trimmed); + } + current = ""; + i++; + if (i < setCookie.length && setCookie[i] === " ") { + i++; + } + } + continue; + } + + current += c; + i++; + } + + const trimmed = current.trim(); + if (trimmed) { + result.push(trimmed); + } + + return result; +} + export function parseSetCookieHeader( setCookie: string, ): Map { const cookies = new Map(); - const cookieArray = setCookie.split(", "); + const cookieArray = splitSetCookieHeader(setCookie); cookieArray.forEach((cookieString) => { const parts = cookieString.split(";").map((part) => part.trim()); @@ -107,12 +151,9 @@ export function setCookieToHeader(headers: Headers) { } }); - const setCookieHeaders = setCookieHeader.split(","); - setCookieHeaders.forEach((header) => { - const cookies = parseSetCookieHeader(header); - cookies.forEach((value, name) => { - cookieMap.set(name, value.value); - }); + const cookies = parseSetCookieHeader(setCookieHeader); + cookies.forEach((value, name) => { + cookieMap.set(name, value.value); }); const updatedCookies = Array.from(cookieMap.entries()) diff --git a/packages/better-auth/src/cookies/cookies.test.ts b/packages/better-auth/src/cookies/cookies.test.ts index 66ddfc7600..8e16c78e79 100644 --- a/packages/better-auth/src/cookies/cookies.test.ts +++ b/packages/better-auth/src/cookies/cookies.test.ts @@ -219,6 +219,24 @@ describe("cookie-utils parseSetCookieHeader", () => { const map = parseSetCookieHeader(header); expect(map.get("a")?.value).toBe("1"); expect(map.get("b")?.value).toBe("2"); + expect(map.get("a")?.expires).toEqual( + new Date("Wed, 21 Oct 2015 07:28:00 GMT"), + ); + expect(map.get("b")?.expires).toEqual( + new Date("Thu, 22 Oct 2015 07:28:00 GMT"), + ); + }); + + it("handles cookie with Expires followed by cookie without Expires", () => { + const map = parseSetCookieHeader( + "session=xyz; Expires=Mon, 01 Jan 2026 00:00:00 GMT, token=abc", + ); + expect(map.get("session")?.value).toBe("xyz"); + expect(map.get("session")?.expires).toEqual( + new Date("Mon, 01 Jan 2026 00:00:00 GMT"), + ); + expect(map.get("token")?.value).toBe("abc"); + expect(map.get("token")?.expires).toBeUndefined(); }); }); diff --git a/packages/expo/src/client.ts b/packages/expo/src/client.ts index 860f56a3ac..9c7cd4d0fd 100644 --- a/packages/expo/src/client.ts +++ b/packages/expo/src/client.ts @@ -5,6 +5,7 @@ import type { } from "@better-auth/core"; import { safeJSONParse } from "@better-auth/core/utils"; import { + parseSetCookieHeader, SECURE_COOKIE_PREFIX, stripSecureCookiePrefix, } from "better-auth/cookies"; @@ -19,79 +20,6 @@ if (Platform.OS !== "web") { setupExpoOnlineManager(); } -interface CookieAttributes { - value: string; - expires?: Date | undefined; - "max-age"?: number | undefined; - domain?: string | undefined; - path?: string | undefined; - secure?: boolean | undefined; - httpOnly?: boolean | undefined; - sameSite?: ("Strict" | "Lax" | "None") | undefined; -} - -export function parseSetCookieHeader( - header: string, -): Map { - const cookieMap = new Map(); - const cookies = splitSetCookieHeader(header); - cookies.forEach((cookie) => { - const parts = cookie.split(";").map((p) => p.trim()); - const [nameValue, ...attributes] = parts; - const [name, ...valueParts] = (nameValue || "").split("="); - const value = valueParts.join("="); - - if (!name || value === undefined) { - return; - } - - const attrObj: CookieAttributes = { value }; - attributes.forEach((attr) => { - const [attrName, ...attrValueParts] = attr.split("="); - if (!attrName?.trim()) { - return; - } - const attrValue = attrValueParts.join("="); - const normalizedAttrName = attrName.trim().toLowerCase(); - attrObj[normalizedAttrName as "value"] = attrValue; - }); - cookieMap.set(name, attrObj); - }); - return cookieMap; -} - -function splitSetCookieHeader(setCookie: string): string[] { - const parts: string[] = []; - let buffer = ""; - let i = 0; - while (i < setCookie.length) { - const char = setCookie[i]; - if (char === ",") { - const recent = buffer.toLowerCase(); - const hasExpires = recent.includes("expires="); - const hasGmt = /gmt/i.test(recent); - if (hasExpires && !hasGmt) { - buffer += char; - i += 1; - continue; - } - if (buffer.trim().length > 0) { - parts.push(buffer.trim()); - buffer = ""; - } - i += 1; - if (setCookie[i] === " ") i += 1; - continue; - } - buffer += char; - i += 1; - } - if (buffer.trim().length > 0) { - parts.push(buffer.trim()); - } - return parts; -} - interface ExpoClientOptions { scheme?: string | undefined; storage: { @@ -538,5 +466,6 @@ export const expoClient = (opts: ExpoClientOptions) => { } satisfies BetterAuthClientPlugin; }; +export { parseSetCookieHeader } from "better-auth/cookies"; export * from "./focus-manager"; export * from "./online-manager";