fix(bearer): write one entry per cookie name when merging session token (#9387)

This commit is contained in:
Taesu
2026-05-01 11:13:16 +09:00
committed by GitHub
parent fe64413cef
commit 906b7b34a7
4 changed files with 81 additions and 8 deletions

View File

@@ -0,0 +1,9 @@
---
"better-auth": patch
---
The bearer plugin now produces a single entry per cookie name when merging
its session token into the request `Cookie` header. Previously the merged
header could carry two entries for the same name if the request already
had a stale session cookie, which would surface to downstream code that
picks the first occurrence.

View File

@@ -173,6 +173,34 @@ export function toCookieOptions(
};
}
/**
* Add or replace a cookie in the request `Cookie` header.
*
* Cookie pairs are joined with `; `, but `headers.append("cookie", ...)`
* joins with `, ` in some runtimes (e.g. Deno, Cloudflare Workers) and
* breaks downstream cookie parsing. This builds the header value via
* parse-mutate-serialize.
*/
export function setRequestCookie(
headers: Headers,
name: string,
value: string,
): void {
const cookieMap = new Map<string, string>();
for (const pair of (headers.get("cookie") || "").split(";")) {
const trimmed = pair.trim();
const eq = trimmed.indexOf("=");
if (eq > 0) cookieMap.set(trimmed.slice(0, eq), trimmed.slice(eq + 1));
}
cookieMap.set(name, value);
headers.set(
"cookie",
Array.from(cookieMap, ([k, v]) => `${k}=${v}`).join("; "),
);
}
export function setCookieToHeader(headers: Headers) {
return (context: { response: Response }) => {
const setCookieHeader = context.response.headers.get("set-cookie");

View File

@@ -12,6 +12,7 @@ import {
HOST_COOKIE_PREFIX,
parseSetCookieHeader,
SECURE_COOKIE_PREFIX,
setRequestCookie,
stripSecureCookiePrefix,
toCookieOptions,
} from "./cookie-utils";
@@ -372,6 +373,44 @@ describe("cookie-utils stripSecureCookiePrefix", () => {
});
});
/**
* @see https://github.com/better-auth/better-call/issues/54
* @see https://github.com/better-auth/better-auth/pull/8089
*/
describe("cookie-utils setRequestCookie", () => {
it("writes a cookie when the header is empty", () => {
const headers = new Headers();
setRequestCookie(headers, "better-auth.session_token", "abc");
expect(headers.get("cookie")).toBe("better-auth.session_token=abc");
});
it("preserves existing cookies and joins with `; ` per RFC 6265", () => {
const headers = new Headers({ cookie: "preference=dark; locale=en" });
setRequestCookie(headers, "better-auth.session_token", "abc");
expect(headers.get("cookie")).toBe(
"preference=dark; locale=en; better-auth.session_token=abc",
);
});
it("replaces an existing cookie of the same name rather than duplicating it", () => {
const headers = new Headers({
cookie: "better-auth.session_token=stale; locale=en",
});
setRequestCookie(headers, "better-auth.session_token", "fresh");
expect(headers.get("cookie")).toBe(
"better-auth.session_token=fresh; locale=en",
);
});
it("ignores malformed pairs in the existing header", () => {
const headers = new Headers({ cookie: "valid=1; ; =orphan; locale=en" });
setRequestCookie(headers, "better-auth.session_token", "abc");
expect(headers.get("cookie")).toBe(
"valid=1; locale=en; better-auth.session_token=abc",
);
});
});
describe("getSessionCookie", async () => {
it("should return the correct session cookie", async () => {
const { signInWithTestUser } = await getTestInstance();

View File

@@ -3,6 +3,7 @@ import { createAuthMiddleware } from "@better-auth/core/api";
import { createHMAC } from "@better-auth/utils/hmac";
import { serializeSignedCookie } from "better-call";
import { parseSetCookieHeader } from "../../cookies";
import { setRequestCookie } from "../../cookies/cookie-utils";
import { PACKAGE_VERSION } from "../../version";
declare module "@better-auth/core" {
@@ -105,14 +106,10 @@ export const bearer = (options?: BearerOptions | undefined) => {
const headers = new Headers({
...Object.fromEntries(existingHeaders?.entries()),
});
// Use headers.set() with "; " separator per RFC 6265.
// headers.append("cookie") joins with ", " in some runtimes
// (e.g. Deno, Cloudflare Workers), which breaks cookie parsing.
const existingCookie = headers.get("cookie");
const newCookie = `${c.context.authCookies.sessionToken.name}=${signedToken}`;
headers.set(
"cookie",
existingCookie ? `${existingCookie}; ${newCookie}` : newCookie,
setRequestCookie(
headers,
c.context.authCookies.sessionToken.name,
signedToken,
);
return {
context: {