diff --git a/docs/content/docs/concepts/cookies.mdx b/docs/content/docs/concepts/cookies.mdx index 4a6364d1b8..0da67ebc22 100644 --- a/docs/content/docs/concepts/cookies.mdx +++ b/docs/content/docs/concepts/cookies.mdx @@ -50,90 +50,18 @@ export const auth = betterAuth({ }) ``` -### Cross-Domain Cookies - -Sometimes you may need to share cookies across subdomains. -For example, if you authenticate on `auth.example.com`, you may also want to access the same session on `app.example.com`. - -Better Auth provides two approaches for this: - -#### Option 1: Using crossOriginCookies (Recommended) - -```ts title="auth.ts" -import { betterAuth } from "better-auth" - -export const auth = betterAuth({ - advanced: { - crossOriginCookies: { - enabled: true, - autoSecure: true, // Automatically set secure=true for SameSite=None - allowLocalhostUnsecure: true, // Allow unsecure cookies on localhost for development - }, - defaultCookieAttributes: { - sameSite: "none", // Required for cross-domain cookies - // secure: true is automatically set when autoSecure: true - partitioned: true, // Recommended for modern browsers - }, - }, - trustedOrigins: [ - 'https://app.mydomain.com', - 'https://admin.mydomain.com', - // Add all your frontend domains - ], -}) -``` - -#### Option 2: Manual Configuration - -```ts title="auth.ts" -import { betterAuth } from "better-auth" - -export const auth = betterAuth({ - advanced: { - defaultCookieAttributes: { - secure: true, - httpOnly: true, - sameSite: "none", // Allows cross-domain cookie sharing - partitioned: true, // New browser standards will mandate this for foreign cookies - }, - }, - trustedOrigins: [ - 'https://app.mydomain.com', - 'https://admin.otherdomain.com', - ], -}) -``` - - -**Important Requirements for Cross-Domain Cookies:** -- `SameSite=None` cookies **must** have `Secure=true` (except on localhost for development) -- Your API must be served over HTTPS (except localhost) -- You must configure CORS on your backend with `credentials: true` -- Your frontend must use `credentials: 'include'` in fetch requests -- All frontend domains must be added to `trustedOrigins` - - -Setting your `sameSite` cookie attribute to `none` makes you vulnerable to CSRF attacks. To mitigate risks, configure `trustedOrigins` with an array of authorized origins to allow. - ### Cross Subdomain Cookies -Sometimes you may need to share cookies across subdomains. For example, if you have `app.example.com` and `example.com`, and if you authenticate on `example.com`, you may want to access the same session on `app.example.com`. - -By default, cookies are not shared between subdomains. However, if you need to access the same session across different subdomains, you can enable cross-subdomain cookies by configuring `crossSubDomainCookies` and `defaultCookieAttributes` in the `advanced` object of the auth options. - -The leading period of the `domain` attribute broadens the cookie's scope beyond your main domain, making it accessible across all subdomains. - - - If your frontend and backend are on different domains, you must configure CORS on your backend, set credentials: 'include' in your frontend requests, and add your frontend domain to trustedOrigins. See the example above for cookie attributes and trustedOrigins usage. - +Sometimes you may need to share cookies across subdomains. +For example, if you authenticate on `auth.example.com`, you may also want to access the same session on `app.example.com`. - The `domain` attribute controls which domains can access the cookie. Setting it to your root domain (e.g. `example.com`) makes the cookie accessible across all subdomains. For security, follow these guidelines: +The `domain` attribute controls which domains can access the cookie. Setting it to your root domain (e.g. `example.com`) makes the cookie accessible across all subdomains. For security, follow these guidelines: - 1. Only enable cross-subdomain cookies if it's necessary - 2. Set the domain to the most specific scope needed (e.g. `app.example.com` instead of `.example.com`) - 3. Be cautious of untrusted subdomains that could potentially access these cookies - 4. Consider using separate domains for untrusted services (e.g. `status.company.com` vs `app.company.com`) +1. Only enable cross-subdomain cookies if it's necessary +2. Set the domain to the most specific scope needed (e.g. `app.example.com` instead of `.example.com`) +3. Be cautious of untrusted subdomains that could potentially access these cookies +4. Consider using separate domains for untrusted services (e.g. `status.company.com` vs `app.company.com`) ```ts title="auth.ts" @@ -143,12 +71,7 @@ export const auth = betterAuth({ advanced: { crossSubDomainCookies: { enabled: true, - domain: ".example.com", // Domain with a leading period - }, - defaultCookieAttributes: { - secure: true, - httpOnly: true, - sameSite: "lax", // Lax is safe for subdomains + domain: "app.example.com", // your domain }, }, trustedOrigins: [ @@ -158,7 +81,6 @@ export const auth = betterAuth({ ], }) ``` - ### Secure Cookies By default, cookies are secure only when the server is running in production mode. You can force cookies to be always secure by setting `useSecureCookies` to `true` in the `advanced` object in the auth options. diff --git a/e2e/integration/vanilla-node/e2e/app.ts b/e2e/integration/vanilla-node/e2e/app.ts index 0dd673d35d..29afd6395d 100644 --- a/e2e/integration/vanilla-node/e2e/app.ts +++ b/e2e/integration/vanilla-node/e2e/app.ts @@ -1,15 +1,11 @@ import { createServer } from "node:http"; -import { createServer as createHttpsServer } from "node:https"; import { betterAuth } from "better-auth"; import { toNodeHandler } from "better-auth/node"; import Database from "better-sqlite3"; import { getMigrations } from "better-auth/db"; -import fs from "node:fs/promises"; -import { join } from "path"; export async function createAuthServer( baseURL: string = "http://localhost:3000", - https: boolean = false, ) { const database = new Database(":memory:"); @@ -19,19 +15,6 @@ export async function createAuthServer( emailAndPassword: { enabled: true, }, - advanced: - https === true - ? { - crossOriginCookies: { - enabled: true, - }, - defaultCookieAttributes: { - sameSite: "none", - partitioned: true, - }, - } - : {}, - trustedOrigins: ["https://test.com:3000"], }); const { runMigrations } = await getMigrations(auth.options); @@ -47,36 +30,6 @@ export async function createAuthServer( }); const authHandler = toNodeHandler(auth); - if (https) { - const options = { - key: await fs.readFile( - join(import.meta.dirname, "fixtures", "private-key.pem"), - ), - cert: await fs.readFile( - join(import.meta.dirname, "fixtures", "certificate.pem"), - ), - }; - return createHttpsServer(options, async (req, res) => { - res.setHeader("Access-Control-Allow-Origin", req.headers.origin || "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type"); - res.setHeader("Access-Control-Allow-Credentials", "true"); - - if (req.method === "OPTIONS") { - res.statusCode = 200; - res.end(); - return; - } - - const isAuthRoute = req.url?.startsWith("/api/auth"); - if (isAuthRoute) { - return authHandler(req, res); - } - - res.statusCode = 404; - res.end(JSON.stringify({ error: "Not found" })); - }); - } return createServer(async (req, res) => { res.setHeader("Access-Control-Allow-Origin", req.headers.origin || "*"); diff --git a/e2e/integration/vanilla-node/e2e/domain.spec.ts b/e2e/integration/vanilla-node/e2e/domain.spec.ts index 79d46e1fa4..d7d9d3933c 100644 --- a/e2e/integration/vanilla-node/e2e/domain.spec.ts +++ b/e2e/integration/vanilla-node/e2e/domain.spec.ts @@ -3,26 +3,18 @@ import { runClient, setup } from "./utils"; const { ref, start, clean } = setup(); test.describe("cross domain", async () => { - test.beforeEach(async () => - start({ - baseURL: "https://demo.com", - https: true, - }), - ); + test.beforeEach(async () => start()); test.afterEach(async () => clean()); test("should work across domains", async () => { const browser = await chromium.launch({ - args: [ - "--host-resolver-rules=MAP * localhost", - "--ignore-certificate-errors", - ], + args: [`--host-resolver-rules=MAP * localhost`], }); const page = await browser.newPage(); await page.goto( - `https://test.com:${ref.clientPort}/?port=${ref.serverPort}&https=1`, + `http://test.com:${ref.clientPort}/?port=${ref.serverPort}`, ); await page.locator("text=Ready").waitFor(); @@ -39,25 +31,10 @@ test.describe("cross domain", async () => { }), ); - // Check that the session is set + // Check that the session is not set because of we didn't set the cookie domain correctly const cookies = await page.context().cookies(); expect( - cookies.find((c) => c.name === "__Secure-better-auth.session_token"), - ).toBeDefined(); - - // Check that we can get the session - await expect( - runClient(page, async ({ client }) => client.getSession()), - ).resolves.toEqual( - expect.objectContaining({ - data: expect.objectContaining({ - session: expect.objectContaining({}), - user: expect.objectContaining({ - email: "test@test.com", - }), - }), - error: null, - }), - ); + cookies.find((c) => c.name === "better-auth.session_token"), + ).not.toBeDefined(); }); }); diff --git a/e2e/integration/vanilla-node/e2e/fixtures/certificate.pem b/e2e/integration/vanilla-node/e2e/fixtures/certificate.pem deleted file mode 100644 index fc503aa2a2..0000000000 --- a/e2e/integration/vanilla-node/e2e/fixtures/certificate.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDCTCCAfGgAwIBAgIUVqpzTWbcJSV2HHoj4FH++LY8TQEwDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDkwOTIyMTYwOVoXDTI1MTAw -OTIyMTYwOVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEA0T8Dqw0HFo3Sh7ZTAfMBk4ENSY7zDhqW6iQgXwHGyI4j -ah40KlHOqB5YKvkrNrJ5aIDdaZy39DzEeEmTnFLsQgVhE+b9A38maeqL+GEJLM4n -vEcKb2m8MgC4NdmnXwdKeEItF4sHQxMicL9LKqNTJMokQWg5UWuBdfcAl8iiK+uD -YMai5OYlvC8hTdq+7I26xGO49uyTuDQHU9PCBEZGvU9BfE7zWX44H68vwcVuaxWi -dGcNPiJZ6KdQa3c0vLbCtkEHoHrQj7s+6xGJT9nVcfjjVjvJFLsM4T3h7oAvebdq -tkJsCje91/WTRdf5TvxIR+uJOz8R3/tq9X1gHale9QIDAQABo1MwUTAdBgNVHQ4E -FgQUDukPU6fLX/U2L1G45aAYPyEJLOAwHwYDVR0jBBgwFoAUDukPU6fLX/U2L1G4 -5aAYPyEJLOAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAUNux -mcwxjOYDKm/6xxbe9EMsZrvLX86yD/wmbvyRZgE5wEn+pD/tgOtFWWNg/QNZwtbr -pwxKYWNRt3O+AXqt0RoPij+Ntn1X6hGp2NMbfVgRJ9HvP6u5CasGxA3AZBrKHtGB -5GLUFB/8CxdARUCML/R+JCrw1howMOl9KJ8s9T1fJic+kKm2g/fZ0pK6RV/nBkkt -veUCgPy0jxN6NcnHN9LTt9Np49sywdaQb2CchB13UyCw3EuBy0kxlyVM2x/wxCzT -y+D7ujdAQqDCJriVHH/XNG7J6633XiR+zRLI0yaPet3ZNp+sl+MSoyAAWz9ENfPg -fWS2EK3F8VkEwru2cQ== ------END CERTIFICATE----- diff --git a/e2e/integration/vanilla-node/e2e/fixtures/private-key.pem b/e2e/integration/vanilla-node/e2e/fixtures/private-key.pem deleted file mode 100644 index 3debb7537e..0000000000 --- a/e2e/integration/vanilla-node/e2e/fixtures/private-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDRPwOrDQcWjdKH -tlMB8wGTgQ1JjvMOGpbqJCBfAcbIjiNqHjQqUc6oHlgq+Ss2snlogN1pnLf0PMR4 -SZOcUuxCBWET5v0DfyZp6ov4YQkszie8RwpvabwyALg12adfB0p4Qi0XiwdDEyJw -v0sqo1MkyiRBaDlRa4F19wCXyKIr64NgxqLk5iW8LyFN2r7sjbrEY7j27JO4NAdT -08IERka9T0F8TvNZfjgfry/BxW5rFaJ0Zw0+Ilnop1BrdzS8tsK2QQegetCPuz7r -EYlP2dVx+ONWO8kUuwzhPeHugC95t2q2QmwKN73X9ZNF1/lO/EhH64k7PxHf+2r1 -fWAdqV71AgMBAAECggEAQfGoCEpMx5DenqKZOm4JOyFavKnuc61xjkzgXtDCgwY4 -pwLp0cSCan1VsAVi1xQcKxEhtl0LpE9aY8ZcXNHNX2kulQUhC+8QfiTLA/Yg3BwE -bdO9uCsMgn2zkFUN6Gb10zyX1WmdWkg3fGztFXzvgRKrpmlgVwaYgELdzvgOE+TN -VDhToR301o9kHbRjXaDwSlj9NXZIYMXad5HvQHqaQ4eiPz3mxTb+9KRGzzs5E/e1 -qxgReaTirA20Ommqpmagp0ymDcppdNIYqdi0an0Msdb322QEVkNkiva/WEt2dyaN -RkFii2QL6WCY276ffLb8DLsrWnWvxS3au1WBGKS/KQKBgQDr1/FdJlUrB0GiGddY -ghhIGmS71ns8z3qXFHm8q2zQm7390de/fpQm0wL98boJ3vWPqmYGvsqAxdkSOY/G -SOfZnQeGT4edK7baMCBd2fupz4uo7SO5GeUkGtKlXynY/JRiiiL9kBnr02T9bsye -pt0/1YCXDCg+Qvv1KhXXWmS6swKBgQDjISSzEo7SvWchSZLkfpWAZ/2n2HxGCgnX -AbIJrhoaW/KdcTNHjIIl+nSsU/LMQFvxvCAN/MXiZULl99LkhfzhvKtx91Sh4vHC -HNGCrfK0yHuJEBOtlle/ACMrhEVsDEhvECb1JeylmjZGT7m7cVSxSn3iUsXi7CFZ -LEDui9DztwKBgHyKnDrEc1U1j8M6Qi3LBWeOCIPoz0tdhS8i8OZg4CWJGNtgZL4K -6dFWvM29SLDNvH8RTK9jMSyvTeIB45qiBU01TzPt639kBjtSDsWAxxT1NJsQHz0w -H4DhaTtK9Z5QQ0uGBtQ/tHCEQWBTklp1Y4DCDhW1pcq7NLJSdxYx9d9hAoGAAd/7 -KpNDpI+1szulP8efulChcmpCFF3u2NyFuLi8NG1/4jbWAogxZtjbb7S9s1vwpl00 -MolcpNLLK5TRI9vLS53AGI+/Hhr+fei4/ggrkbx58Ja2Yt9z1/rLT4em/w5W5rvO -ZoWqn29XdVshIF/+H/yKb3zZb1cdq1vHKXD0B58CgYEA1rSgsOB/WUdCZL5QgiYs -Our713/AsI65BzDGVqfBXhXpulEffzC7GT4Gdp0WOguCm6iC5WFeWMs5/AxpvFmT -qc5/Wq57wOt0E9CG5aaXbZHQ/tWENEdHFThV4rBO8xFqUZJnUxLuSIyzQRAO2o33 -HAQhmXetMKmi4igeSsWECQE= ------END PRIVATE KEY----- diff --git a/e2e/integration/vanilla-node/e2e/utils.ts b/e2e/integration/vanilla-node/e2e/utils.ts index 39f728fa68..cd664786e0 100644 --- a/e2e/integration/vanilla-node/e2e/utils.ts +++ b/e2e/integration/vanilla-node/e2e/utils.ts @@ -26,15 +26,11 @@ export function setup() { }; return { ref, - start: async (options?: { baseURL?: string; https?: boolean }) => { - server = await createAuthServer(options?.baseURL, options?.https); + start: async () => { + server = await createAuthServer(); clientChild = spawn("pnpm", ["run", "start:client"], { cwd: root, stdio: "pipe", - env: { - ...process.env, - HTTPS: options?.https ? "1" : "0", - }, }); clientChild.stderr.on("data", (data) => { const message = data.toString(); @@ -51,7 +47,6 @@ export function setup() { const address = server.address(); if (address && typeof address === "object") { ref.serverPort = address.port; - console.log(`Server listening on port ${ref.serverPort}`); resolve(); } }); @@ -60,14 +55,11 @@ export function setup() { clientChild.stdout.on("data", (data) => { const message = data.toString(); // find: http://localhost:5173/ - if ( - message.includes("http://localhost:") || - message.includes("https://localhost:") - ) { + if (message.includes("http://localhost:")) { const port: string = message - .split("http")[1] - .split("localhost:")[1] - .split("/")[0]; + .split("http://localhost:")[1] + .split("/")[0] + .trim(); ref.clientPort = Number(port.replace(/\x1b\[[0-9;]*m/g, "")); resolve(); } diff --git a/e2e/integration/vanilla-node/src/main.ts b/e2e/integration/vanilla-node/src/main.ts index fd840131cb..beaf587592 100644 --- a/e2e/integration/vanilla-node/src/main.ts +++ b/e2e/integration/vanilla-node/src/main.ts @@ -2,10 +2,9 @@ import { createAuthClient } from "better-auth/client"; const search = new URLSearchParams(window.location.search); const port = search.get("port"); -const https = search.get("https"); const client = createAuthClient({ - baseURL: `http${https ? "s" : ""}://localhost:${port ?? 3000}`, + baseURL: `http://localhost:${port ?? 3000}`, }); declare global { diff --git a/e2e/integration/vanilla-node/vite.config.ts b/e2e/integration/vanilla-node/vite.config.ts index 93b9762136..d7b3ba2ee3 100644 --- a/e2e/integration/vanilla-node/vite.config.ts +++ b/e2e/integration/vanilla-node/vite.config.ts @@ -3,12 +3,5 @@ import { defineConfig } from "vite"; export default defineConfig({ server: { allowedHosts: ["test.com", "localhost"], - https: - process.env.HTTPS === "1" - ? { - key: "./e2e/fixtures/private-key.pem", - cert: "./e2e/fixtures/certificate.pem", - } - : undefined, }, }); diff --git a/packages/better-auth/src/cookies/cookies.test.ts b/packages/better-auth/src/cookies/cookies.test.ts index 61a9ef148c..aec8db54a0 100644 --- a/packages/better-auth/src/cookies/cookies.test.ts +++ b/packages/better-auth/src/cookies/cookies.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; -import { getCookieCache, getCookies, getSessionCookie } from "../cookies"; import { getTestInstance } from "../test-utils/test-instance"; -import type { BetterAuthOptions } from "../types/options"; +import { getCookieCache, getCookies, getSessionCookie } from "../cookies"; import { parseSetCookieHeader } from "./cookie-utils"; +import type { BetterAuthOptions } from "../types/options"; describe("cookies", async () => { const { client, testUser } = await getTestInstance(); @@ -369,142 +369,3 @@ describe("getSessionCookie", async () => { await expect(getCookieCache(request)).rejects.toThrow(); }); }); - -describe("cross-origin cookies", () => { - it("should handle sameSite=none with secure=true correctly", async () => { - const { client, testUser } = await getTestInstance({ - baseURL: "https://api.example.com", - advanced: { - defaultCookieAttributes: { - sameSite: "none", - secure: true, - }, - }, - }); - - await client.signIn.email( - { - email: testUser.email, - password: testUser.password, - }, - { - onResponse(context) { - const setCookie = context.response.headers.get("set-cookie"); - expect(setCookie).toContain("SameSite=None"); - expect(setCookie).toContain("Secure"); - }, - }, - ); - }); - - it("should auto-enforce secure for sameSite=none", async () => { - const { client, testUser } = await getTestInstance({ - baseURL: "https://api.example.com", - advanced: { - crossOriginCookies: { - enabled: true, - autoSecure: true, - }, - defaultCookieAttributes: { - sameSite: "none", - // secure not explicitly set - should be auto-enforced - }, - }, - }); - - await client.signIn.email( - { - email: testUser.email, - password: testUser.password, - }, - { - onResponse(context) { - const setCookie = context.response.headers.get("set-cookie"); - expect(setCookie).toContain("SameSite=None"); - expect(setCookie).toContain("Secure"); - }, - }, - ); - }); - - it("should allow unsecure cookies on localhost for development", async () => { - const { client, testUser } = await getTestInstance({ - baseURL: "http://localhost:3000", - advanced: { - crossOriginCookies: { - enabled: true, - autoSecure: true, - allowLocalhostUnsecure: true, - }, - defaultCookieAttributes: { - sameSite: "none", - secure: false, - }, - }, - }); - - await client.signIn.email( - { - email: testUser.email, - password: testUser.password, - }, - { - onResponse(context) { - const setCookie = context.response.headers.get("set-cookie"); - expect(setCookie).toContain("SameSite=None"); - // Should not contain Secure on localhost when allowLocalhostUnsecure is true - expect(setCookie).not.toContain("Secure"); - }, - }, - ); - }); - - it("should throw error for sameSite=none without secure on non-localhost", async () => { - await expect(async () => { - await getTestInstance({ - baseURL: "https://api.example.com", - advanced: { - crossOriginCookies: { - enabled: true, - autoSecure: false, // Disable auto-secure - }, - defaultCookieAttributes: { - sameSite: "none", - secure: false, // Explicitly set to false - }, - }, - }); - }).rejects.toThrow("SameSite=None cookies require Secure=true"); - }); - - it("should work with partitioned attribute for cross-origin", async () => { - const { client, testUser } = await getTestInstance({ - baseURL: "https://api.example.com", - advanced: { - crossOriginCookies: { - enabled: true, - }, - defaultCookieAttributes: { - sameSite: "none", - secure: true, - partitioned: true, - }, - }, - }); - - await client.signIn.email( - { - email: testUser.email, - password: testUser.password, - }, - { - onResponse(context) { - const setCookie = context.response.headers.get("set-cookie"); - expect(setCookie).toContain("SameSite=None"); - expect(setCookie).toContain("Secure"); - expect(setCookie).toContain("Partitioned"); - }, - }, - ); - }); -}); diff --git a/packages/better-auth/src/cookies/index.ts b/packages/better-auth/src/cookies/index.ts index b63bb619f3..4d3e0d2807 100644 --- a/packages/better-auth/src/cookies/index.ts +++ b/packages/better-auth/src/cookies/index.ts @@ -1,16 +1,16 @@ -import { base64Url } from "@better-auth/utils/base64"; -import { binary } from "@better-auth/utils/binary"; -import { createHMAC } from "@better-auth/utils/hmac"; import type { CookieOptions } from "better-call"; -import { ms } from "ms"; import { BetterAuthError } from "../error"; import type { Session, User } from "../types"; import type { GenericEndpointContext } from "../types/context"; import type { BetterAuthOptions } from "../types/options"; import { getDate } from "../utils/date"; import { env, isProduction } from "../utils/env"; +import { base64Url } from "@better-auth/utils/base64"; +import { ms } from "ms"; +import { createHMAC } from "@better-auth/utils/hmac"; import { safeJSONParse } from "../utils/json"; import { getBaseURL } from "../utils/url"; +import { binary } from "@better-auth/utils/binary"; export function createCookieGetter(options: BetterAuthOptions) { const secure = @@ -24,22 +24,10 @@ export function createCookieGetter(options: BetterAuthOptions) { const secureCookiePrefix = secure ? "__Secure-" : ""; const crossSubdomainEnabled = !!options.advanced?.crossSubDomainCookies?.enabled; - const crossOriginEnabled = !!options.advanced?.crossOriginCookies?.enabled; - - // Cross-origin cookie configuration - const crossOriginConfig = { - autoSecure: options.advanced?.crossOriginCookies?.autoSecure ?? true, - allowLocalhostUnsecure: - options.advanced?.crossOriginCookies?.allowLocalhostUnsecure ?? true, - ...options.advanced?.crossOriginCookies, - }; - - const domain = crossOriginEnabled - ? undefined - : crossSubdomainEnabled - ? options.advanced?.crossSubDomainCookies?.domain || - (options.baseURL ? new URL(options.baseURL).hostname : undefined) - : undefined; + const domain = crossSubdomainEnabled + ? options.advanced?.crossSubDomainCookies?.domain || + (options.baseURL ? new URL(options.baseURL).hostname : undefined) + : undefined; if (crossSubdomainEnabled && !domain) { throw new BetterAuthError( "baseURL is required when crossSubdomainCookies are enabled", @@ -57,53 +45,18 @@ export function createCookieGetter(options: BetterAuthOptions) { const attributes = options.advanced?.cookies?.[cookieName as "session_token"]?.attributes; - // Build base attributes - let baseAttributes: CookieOptions = { - secure: !!secureCookiePrefix, - sameSite: "lax", - path: "/", - httpOnly: true, - ...(crossSubdomainEnabled ? { domain } : {}), - }; - - // Merge with user-defined attributes - const mergedAttributes = { - ...baseAttributes, - ...options.advanced?.defaultCookieAttributes, - ...overrideAttributes, - ...attributes, - }; - - // Enforce security constraints for SameSite=None cookies - if (mergedAttributes.sameSite === "none") { - const isLocalhost = - options.baseURL?.startsWith("http://localhost") || - options.baseURL?.startsWith("http://127.0.0.1"); - - // SameSite=None requires Secure=true (except localhost in development) - if (!mergedAttributes.secure) { - if (crossOriginConfig.allowLocalhostUnsecure && isLocalhost) { - // Allow unsecure cookies on localhost for development - } else { - throw new BetterAuthError( - "SameSite=None cookies require Secure=true. Set secure: true in your cookie attributes, use HTTPS, or disable crossOriginCookies.autoSecure for development.", - ); - } - } - - // Auto-set secure for SameSite=None (except localhost for development) - if (crossOriginConfig.autoSecure && !isLocalhost) { - mergedAttributes.secure = true; - } - } - - // Apply secure cookie prefix if secure is enforced - const finalSecurePrefix = mergedAttributes.secure ? "__Secure-" : ""; - const finalName = `${finalSecurePrefix}${name}`; - return { - name: finalName, - attributes: mergedAttributes as CookieOptions, + name: `${secureCookiePrefix}${name}`, + attributes: { + secure: !!secureCookiePrefix, + sameSite: "lax", + path: "/", + httpOnly: true, + ...(crossSubdomainEnabled ? { domain } : {}), + ...options.advanced?.defaultCookieAttributes, + ...overrideAttributes, + ...attributes, + } as CookieOptions, }; } return createCookie; diff --git a/packages/better-auth/src/types/options.ts b/packages/better-auth/src/types/options.ts index aa8a2bd15a..b5c686b910 100644 --- a/packages/better-auth/src/types/options.ts +++ b/packages/better-auth/src/types/options.ts @@ -1,14 +1,4 @@ -import type { CookieOptions } from "better-call"; -import type { Database } from "better-sqlite3"; -import type { Database as BunDatabase } from "bun:sqlite"; import type { Dialect, Kysely, MysqlPool, PostgresPool } from "kysely"; -import type { DatabaseSync } from "node:sqlite"; -import type { AuthContext } from "."; -import type { AdapterDebugLogs } from "../adapters"; -import type { KyselyDatabaseType } from "../adapters/kysely-adapter/types"; -import type { FieldAttribute } from "../db"; -import type { AuthMiddleware } from "../plugins"; -import type { SocialProviderList, SocialProviders } from "../social-providers"; import type { Account, GenericEndpointContext, @@ -16,11 +6,21 @@ import type { User, Verification, } from "../types"; -import type { Logger } from "../utils"; -import type { AdapterInstance, SecondaryStorage } from "./adapter"; -import type { LiteralUnion, OmitId } from "./helper"; -import type { Models, RateLimit } from "./models"; import type { BetterAuthPlugin } from "./plugins"; +import type { SocialProviderList, SocialProviders } from "../social-providers"; +import type { AdapterInstance, SecondaryStorage } from "./adapter"; +import type { KyselyDatabaseType } from "../adapters/kysely-adapter/types"; +import type { FieldAttribute } from "../db"; +import type { Models, RateLimit } from "./models"; +import type { AuthContext } from "."; +import type { CookieOptions } from "better-call"; +import type { Database } from "better-sqlite3"; +import type { Logger } from "../utils"; +import type { AuthMiddleware } from "../plugins"; +import type { LiteralUnion, OmitId } from "./helper"; +import type { AdapterDebugLogs } from "../adapters"; +import type { Database as BunDatabase } from "bun:sqlite"; +import type { DatabaseSync } from "node:sqlite"; export type BetterAuthOptions = { /** @@ -711,30 +711,6 @@ export type BetterAuthOptions = { */ domain?: string; }; - /** - * Configure cookies for cross-origin scenarios - * - * This enables proper handling of SameSite=None cookies - * when your API and frontend are on different domains. - */ - crossOriginCookies?: { - /** - * Enable cross-origin cookie handling - */ - enabled: boolean; - /** - * Automatically set Secure=true when SameSite=None is detected - * - * @default true - */ - autoSecure?: boolean; - /** - * Allow localhost without secure cookies (for development) - * - * @default true - */ - allowLocalhostUnsecure?: boolean; - }; /* * Allows you to change default cookie names and attributes *