mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-25 10:13:54 -05:00
Revert "feat: support cross-domain cookie (#4482)"
This reverts commit 61b6a87435.
This commit is contained in:
@@ -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',
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
**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`
|
||||
</Callout>
|
||||
|
||||
<Callout type="warn">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.</Callout>
|
||||
|
||||
### 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.
|
||||
|
||||
<Callout>The leading period of the `domain` attribute broadens the cookie's scope beyond your main domain, making it accessible across all subdomains.</Callout>
|
||||
|
||||
<Callout type="note">
|
||||
If your frontend and backend are on different domains, you must configure CORS on your backend, set <code>credentials: 'include'</code> in your frontend requests, and add your frontend domain to <code>trustedOrigins</code>. See the example above for cookie attributes and trustedOrigins usage.
|
||||
</Callout>
|
||||
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`.
|
||||
|
||||
<Callout type="warn">
|
||||
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`)
|
||||
</Callout>
|
||||
|
||||
```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.
|
||||
|
||||
@@ -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 || "*");
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user