mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 23:53:17 -05:00
fix: centralize cookie parsing and handle Expires dates correctly (#7556)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: taesu <taesu@better-auth.com> Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
committed by
Alex Yang
co-authored by
Cursor Agent
taesu
Alex Yang
parent
d2ca08bc48
commit
d598bd03f7
@@ -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<string, CookieAttributes> {
|
||||
const cookies = new Map<string, CookieAttributes>();
|
||||
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())
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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<string, CookieAttributes> {
|
||||
const cookieMap = new Map<string, CookieAttributes>();
|
||||
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";
|
||||
|
||||
Reference in New Issue
Block a user