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
*