mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-23 07:18:56 -05:00
fix(bearer): write one entry per cookie name when merging session token (#9387)
This commit is contained in:
9
.changeset/fix-bearer-cookie-parse-mutate-serialize.md
Normal file
9
.changeset/fix-bearer-cookie-parse-mutate-serialize.md
Normal 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.
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user