diff --git a/packages/better-auth/src/cookies/cookie-utils.ts b/packages/better-auth/src/cookies/cookie-utils.ts index 7342a9a797..b2d9b71c05 100644 --- a/packages/better-auth/src/cookies/cookie-utils.ts +++ b/packages/better-auth/src/cookies/cookie-utils.ts @@ -1,3 +1,11 @@ +function tryDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} + export interface CookieAttributes { value: string; "max-age"?: number | undefined; @@ -85,7 +93,8 @@ export function parseSetCookieHeader( return; } - const attrObj: CookieAttributes = { value }; + const decodedValue = value.includes("%") ? tryDecode(value) : value; + const attrObj: CookieAttributes = { value: decodedValue }; attributes.forEach((attribute) => { const [attrName, ...attrValueParts] = attribute!.split("="); diff --git a/packages/better-auth/src/cookies/cookies.test.ts b/packages/better-auth/src/cookies/cookies.test.ts index bb4f95feed..4ca985d1fa 100644 --- a/packages/better-auth/src/cookies/cookies.test.ts +++ b/packages/better-auth/src/cookies/cookies.test.ts @@ -226,6 +226,12 @@ describe("cookie-utils parseSetCookieHeader", () => { ); }); + it("decodes URI-encoded cookie values", () => { + const header = "token=hello%20world%3Dfoo; Path=/"; + const map = parseSetCookieHeader(header); + expect(map.get("token")?.value).toBe("hello world=foo"); + }); + 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", diff --git a/packages/better-auth/src/integrations/next-js.ts b/packages/better-auth/src/integrations/next-js.ts index 22bda311f1..8c25357cf1 100644 --- a/packages/better-auth/src/integrations/next-js.ts +++ b/packages/better-auth/src/integrations/next-js.ts @@ -100,7 +100,7 @@ export const nextCookies = () => { path: value.path, } as const; try { - cookieHelper.set(key, decodeURIComponent(value.value), opts); + cookieHelper.set(key, value.value, opts); } catch { // this will fail if the cookie is being set on server component } diff --git a/packages/better-auth/src/integrations/svelte-kit.ts b/packages/better-auth/src/integrations/svelte-kit.ts index 88c280f64d..c0090a78e0 100644 --- a/packages/better-auth/src/integrations/svelte-kit.ts +++ b/packages/better-auth/src/integrations/svelte-kit.ts @@ -78,7 +78,7 @@ export const sveltekitCookies = ( for (const [name, { value, ...ops }] of parsed) { try { - event.cookies.set(name, decodeURIComponent(value), { + event.cookies.set(name, value, { sameSite: ops.samesite, path: ops.path || "/", expires: ops.expires, diff --git a/packages/better-auth/src/integrations/tanstack-start-solid.ts b/packages/better-auth/src/integrations/tanstack-start-solid.ts index 5b628a5829..72309ed4c6 100644 --- a/packages/better-auth/src/integrations/tanstack-start-solid.ts +++ b/packages/better-auth/src/integrations/tanstack-start-solid.ts @@ -51,7 +51,7 @@ export const tanstackStartCookies = () => { path: value.path, } as const; try { - setCookie(key, decodeURIComponent(value.value), opts); + setCookie(key, value.value, opts); } catch { // this will fail if the cookie is being set on server component } diff --git a/packages/better-auth/src/integrations/tanstack-start.ts b/packages/better-auth/src/integrations/tanstack-start.ts index 74e83b076a..51cdb7911b 100644 --- a/packages/better-auth/src/integrations/tanstack-start.ts +++ b/packages/better-auth/src/integrations/tanstack-start.ts @@ -51,7 +51,7 @@ export const tanstackStartCookies = () => { path: value.path, } as const; try { - setCookie(key, decodeURIComponent(value.value), opts); + setCookie(key, value.value, opts); } catch { // this will fail if the cookie is being set on server component } diff --git a/packages/better-auth/src/plugins/custom-session/custom-session.test.ts b/packages/better-auth/src/plugins/custom-session/custom-session.test.ts index 53bf81bcb7..dd8fc6fcf8 100644 --- a/packages/better-auth/src/plugins/custom-session/custom-session.test.ts +++ b/packages/better-auth/src/plugins/custom-session/custom-session.test.ts @@ -1,5 +1,6 @@ import { describe, expect, expectTypeOf, it } from "vitest"; import { createAuthClient } from "../../client"; +import { parseSetCookieHeader } from "../../cookies"; import { getTestInstance } from "../../test-utils/test-instance"; import type { BetterAuthOptions } from "../../types"; import { admin } from "../admin"; @@ -81,6 +82,37 @@ describe("Custom Session Plugin Tests", async () => { }); }); + it("should not double-encode session cookie during get-session refresh", async () => { + const { headers } = await signInWithTestUser(); + const signedInCookie = headers.get("cookie"); + const signedInSessionToken = signedInCookie?.match( + /better-auth\.session_token=([^;]+)/, + )?.[1]; + expect(signedInSessionToken).toBeDefined(); + + let refreshedSessionToken: string | undefined; + await client.getSession({ + fetchOptions: { + headers, + onResponse(context) { + const setCookies = context.response.headers.getSetCookie(); + for (const cookieStr of setCookies) { + const parsed = parseSetCookieHeader(cookieStr); + const token = parsed.get("better-auth.session_token")?.value; + if (token) { + refreshedSessionToken = token; + break; + } + } + }, + }, + }); + + expect(refreshedSessionToken).toBeDefined(); + expect(refreshedSessionToken).toBe(signedInSessionToken); + expect(refreshedSessionToken).not.toContain("%25"); + }); + it("should return the custom session for multi-session", async () => { const headers = new Headers(); const testUser = {