Skip to content

Extensive Cookie Configuration #2059

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- [`beforeSessionSaved`](#beforesessionsaved)
- [`onCallback`](#oncallback)
- [Session configuration](#session-configuration)
- [Cookie Configuration](#cookie-configuration)
- [Database sessions](#database-sessions)
- [Back-Channel Logout](#back-channel-logout)
- [Combining middleware](#combining-middleware)
Expand Down Expand Up @@ -685,6 +686,67 @@ export const auth0 = new Auth0Client({
| absoluteDuration | `number` | The absolute duration after which the session will expire. The value must be specified in seconds. Default: `3 days`. |
| inactivityDuration | `number` | The duration of inactivity after which the session will expire. The value must be specified in seconds. Default: `1 day`. |

## Cookie Configuration

You can configure the session cookie attributes either through environment variables or directly in the SDK initialization.

**1. Using Environment Variables:**

Set the desired environment variables in your `.env.local` file or your deployment environment:

```
# .env.local
# ... other variables ...

# Cookie Options
AUTH0_COOKIE_DOMAIN='.example.com' # Set cookie for subdomains
AUTH0_COOKIE_PATH='/app' # Limit cookie to /app path
AUTH0_COOKIE_TRANSIENT=true # Make cookie transient (session-only)
AUTH0_COOKIE_SECURE=true # Recommended for production
AUTH0_COOKIE_SAME_SITE='Lax'
```

The SDK will automatically pick up these values. Note that `httpOnly` is always set to `true` for security reasons and cannot be configured.

**2. Using `Auth0ClientOptions`:**

Configure the options directly when initializing the client:

```typescript
import { Auth0Client } from "@auth0/nextjs-auth0/server"

export const auth0 = new Auth0Client({
session: {
cookie: {
domain: '.example.com',
path: '/app',
transient: true,
// httpOnly is always true and cannot be configured
secure: process.env.NODE_ENV === 'production',
sameSite: 'Lax',
// name: 'appSession', // Optional: custom cookie name, defaults to '__session'
},
// ... other session options like absoluteDuration ...
},
// ... other client options ...
});
```

**Session Cookie Options:**

* `domain` (String): Specifies the `Domain` attribute.
* `path` (String): Specifies the `Path` attribute. Defaults to `/`.
* `transient` (Boolean): If `true`, the `maxAge` attribute is omitted, making it a session cookie. Defaults to `false`.
* `secure` (Boolean): Specifies the `Secure` attribute. Defaults to `false` (or `true` if `AUTH0_COOKIE_SECURE=true` is set).
* `sameSite` ('Lax' | 'Strict' | 'None'): Specifies the `SameSite` attribute. Defaults to `Lax` (or the value of `AUTH0_COOKIE_SAME_SITE`).
* `name` (String): The name of the session cookie. Defaults to `__session`.

> [!INFO]
> Options provided directly in `Auth0ClientOptions` take precedence over environment variables. The `httpOnly` attribute is always `true` regardless of configuration.

> [!INFO]
> The `httpOnly` attribute for the session cookie is always set to `true` for security reasons and cannot be configured via options or environment variables.

## Database sessions

By default, the user's sessions are stored in encrypted cookies. You may choose to persist the sessions in your data store of choice.
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ You can customize the client by using the options below:
| appBaseUrl | `string` | The URL of your application (e.g.: `http://localhost:3000`). If it's not specified, it will be loaded from the `APP_BASE_URL` environment variable. |
| secret | `string` | A 32-byte, hex-encoded secret used for encrypting cookies. If it's not specified, it will be loaded from the `AUTH0_SECRET` environment variable. |
| signInReturnToPath | `string` | The path to redirect the user to after successfully authenticating. Defaults to `/`. |
| session | `SessionConfiguration` | Configure the session timeouts and whether to use rolling sessions or not. See [Session configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#session-configuration) for additional details. |
| session | `SessionConfiguration` | Configure the session timeouts and whether to use rolling sessions or not. See [Session configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#session-configuration) for additional details. Also allows configuration of cookie attributes like `domain`, `path`, `secure`, `sameSite`, and `transient`. If not specified, these can be configured using `AUTH0_COOKIE_*` environment variables. Note: `httpOnly` is always `true`. See [Cookie Configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#cookie-configuration) for details. |
| beforeSessionSaved | `BeforeSessionSavedHook` | A method to manipulate the session before persisting it. See [beforeSessionSaved](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#beforesessionsaved) for additional details. |
| onCallback | `OnCallbackHook` | A method to handle errors or manage redirects after attempting to authenticate. See [onCallback](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#oncallback) for additional details. |
| sessionStore | `SessionStore` | A custom session store implementation used to persist sessions to a data store. See [Database sessions](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#database-sessions) for additional details. |
Expand All @@ -147,6 +147,17 @@ You can customize the client by using the options below:
| httpTimeout | `number` | Integer value for the HTTP timeout in milliseconds for authentication requests. Defaults to `5000` milliseconds |
| enableTelemetry | `boolean` | Boolean value to opt-out of sending the library name and version to your authorization server via the `Auth0-Client` header. Defaults to `true`. |

## Session Cookie Configuration
You can specify the following environment variables to configure the session cookie:
```env
AUTH0_COOKIE_DOMAIN=
AUTH0_COOKIE_PATH=
AUTH0_COOKIE_TRANSIENT=
AUTH0_COOKIE_SECURE=
AUTH0_COOKIE_SAME_SITE=
```
Respective counterparts are also available in the client configuration. See [Cookie Configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#cookie-configuration) for more details.

## Configuration Validation

The SDK performs validation of required configuration options when initializing the `Auth0Client`. The following options are mandatory and must be provided either through constructor options or environment variables:
Expand Down
192 changes: 192 additions & 0 deletions src/server/chunked-cookies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,198 @@ describe("Chunked Cookie Utils", () => {
expect(reqCookies.delete).toHaveBeenCalledWith(name);
});

// New tests for domain and transient options
it("should set the domain property for a single cookie", () => {
const name = "domainCookie";
const value = "small value";
const options: CookieOptions = {
path: "/",
domain: "example.com",
httpOnly: true,
secure: true,
sameSite: "lax"
};

setChunkedCookie(name, value, options, reqCookies, resCookies);

expect(resCookies.set).toHaveBeenCalledTimes(1);
expect(resCookies.set).toHaveBeenCalledWith(
name,
value,
expect.objectContaining({ domain: "example.com" })
);
});

it("should set the domain property for chunked cookies", () => {
const name = "largeDomainCookie";
const largeValue = "a".repeat(8000);
const options: CookieOptions = {
path: "/",
domain: "example.com",
httpOnly: true,
secure: true,
sameSite: "lax"
};

setChunkedCookie(name, largeValue, options, reqCookies, resCookies);

expect(resCookies.set).toHaveBeenCalledTimes(3); // 3 chunks
expect(resCookies.set).toHaveBeenNthCalledWith(
1,
`${name}__0`,
expect.any(String),
expect.objectContaining({ domain: "example.com" })
);
expect(resCookies.set).toHaveBeenNthCalledWith(
2,
`${name}__1`,
expect.any(String),
expect.objectContaining({ domain: "example.com" })
);
expect(resCookies.set).toHaveBeenNthCalledWith(
3,
`${name}__2`,
expect.any(String),
expect.objectContaining({ domain: "example.com" })
);
});

it("should omit maxAge for a single transient cookie", () => {
const name = "transientCookie";
const value = "small value";
const options: CookieOptions = {
path: "/",
maxAge: 3600,
transient: true,
httpOnly: true,
secure: true,
sameSite: "lax"
};
const expectedOptions = { ...options };
delete expectedOptions.maxAge; // maxAge should be removed
delete expectedOptions.transient; // transient flag itself is not part of the cookie options

setChunkedCookie(name, value, options, reqCookies, resCookies);

expect(resCookies.set).toHaveBeenCalledTimes(1);
expect(resCookies.set).toHaveBeenCalledWith(name, value, expectedOptions);
expect(resCookies.set).not.toHaveBeenCalledWith(
name,
value,
expect.objectContaining({ maxAge: 3600 })
);
});

it("should omit maxAge for chunked transient cookies", () => {
const name = "largeTransientCookie";
const largeValue = "a".repeat(8000);
const options: CookieOptions = {
path: "/",
maxAge: 3600,
transient: true,
httpOnly: true,
secure: true,
sameSite: "lax"
};
const expectedOptions = { ...options };
delete expectedOptions.maxAge; // maxAge should be removed
delete expectedOptions.transient; // transient flag itself is not part of the cookie options

setChunkedCookie(name, largeValue, options, reqCookies, resCookies);

expect(resCookies.set).toHaveBeenCalledTimes(3); // 3 chunks
expect(resCookies.set).toHaveBeenNthCalledWith(
1,
`${name}__0`,
expect.any(String),
expectedOptions
);
expect(resCookies.set).toHaveBeenNthCalledWith(
2,
`${name}__1`,
expect.any(String),
expectedOptions
);
expect(resCookies.set).toHaveBeenNthCalledWith(
3,
`${name}__2`,
expect.any(String),
expectedOptions
);
expect(resCookies.set).not.toHaveBeenCalledWith(
expect.any(String),
expect.any(String),
expect.objectContaining({ maxAge: 3600 })
);
});

it("should include maxAge for a single non-transient cookie", () => {
const name = "nonTransientCookie";
const value = "small value";
const options: CookieOptions = {
path: "/",
maxAge: 3600,
transient: false,
httpOnly: true,
secure: true,
sameSite: "lax"
};
const expectedOptions = { ...options };
delete expectedOptions.transient; // transient flag itself is not part of the cookie options

setChunkedCookie(name, value, options, reqCookies, resCookies);

expect(resCookies.set).toHaveBeenCalledTimes(1);
expect(resCookies.set).toHaveBeenCalledWith(name, value, expectedOptions);
expect(resCookies.set).toHaveBeenCalledWith(
name,
value,
expect.objectContaining({ maxAge: 3600 })
);
});

it("should include maxAge for chunked non-transient cookies", () => {
const name = "largeNonTransientCookie";
const largeValue = "a".repeat(8000);
const options: CookieOptions = {
path: "/",
maxAge: 3600,
transient: false,
httpOnly: true,
secure: true,
sameSite: "lax"
};
const expectedOptions = { ...options };
delete expectedOptions.transient; // transient flag itself is not part of the cookie options

setChunkedCookie(name, largeValue, options, reqCookies, resCookies);

expect(resCookies.set).toHaveBeenCalledTimes(3); // 3 chunks
expect(resCookies.set).toHaveBeenNthCalledWith(
1,
`${name}__0`,
expect.any(String),
expectedOptions
);
expect(resCookies.set).toHaveBeenNthCalledWith(
2,
`${name}__1`,
expect.any(String),
expectedOptions
);
expect(resCookies.set).toHaveBeenNthCalledWith(
3,
`${name}__2`,
expect.any(String),
expectedOptions
);
expect(resCookies.set).toHaveBeenCalledWith(
expect.any(String),
expect.any(String),
expect.objectContaining({ maxAge: 3600 })
);
});

describe("getChunkedCookie", () => {
it("should return undefined when cookie does not exist", () => {
const result = getChunkedCookie("nonexistent", reqCookies);
Expand Down
16 changes: 13 additions & 3 deletions src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,19 @@ export class Auth0Client {

const sessionCookieOptions: SessionCookieOptions = {
name: options.session?.cookie?.name ?? "__session",
secure: options.session?.cookie?.secure ?? false,
sameSite: options.session?.cookie?.sameSite ?? "lax",
path: options.session?.cookie?.path ?? "/"
secure:
options.session?.cookie?.secure ??
process.env.AUTH0_COOKIE_SECURE === "true",
sameSite:
options.session?.cookie?.sameSite ??
(process.env.AUTH0_COOKIE_SAME_SITE as "lax" | "strict" | "none") ??
"lax",
path:
options.session?.cookie?.path ?? process.env.AUTH0_COOKIE_PATH ?? "/",
transient:
options.session?.cookie?.transient ??
process.env.AUTH0_COOKIE_TRANSIENT === "true",
domain: options.session?.cookie?.domain ?? process.env.AUTH0_COOKIE_DOMAIN
};

const transactionCookieOptions: TransactionCookieOptions = {
Expand Down
13 changes: 11 additions & 2 deletions src/server/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ export interface CookieOptions {
secure: boolean;
path: string;
maxAge?: number;
domain?: string;
transient?: boolean;
}

export type ReadonlyRequestCookies = Omit<
Expand Down Expand Up @@ -178,11 +180,18 @@ export function setChunkedCookie(
reqCookies: RequestCookies,
resCookies: ResponseCookies
): void {
const { transient, ...restOptions } = options;
const finalOptions = { ...restOptions };

if (transient) {
delete finalOptions.maxAge;
}

const valueBytes = new TextEncoder().encode(value).length;

// If value fits in a single cookie, set it directly
if (valueBytes <= MAX_CHUNK_SIZE) {
resCookies.set(name, value, options);
resCookies.set(name, value, finalOptions);
// to enable read-after-write in the same request for middleware
reqCookies.set(name, value);

Expand All @@ -203,7 +212,7 @@ export function setChunkedCookie(
const chunk = value.slice(position, position + MAX_CHUNK_SIZE);
const chunkName = `${name}${CHUNK_PREFIX}${chunkIndex}`;

resCookies.set(chunkName, chunk, options);
resCookies.set(chunkName, chunk, finalOptions);
// to enable read-after-write in the same request for middleware
reqCookies.set(chunkName, chunk);
position += MAX_CHUNK_SIZE;
Expand Down
14 changes: 13 additions & 1 deletion src/server/session/abstract-session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ export interface SessionCookieOptions {
* The path attribute of the session cookie. Will be set to '/' by default.
*/
path?: string;
/**
* Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.3|Domain Set-Cookie attribute}. By default, no
* domain is set, and most clients will consider the cookie to apply to only
* the current domain.
*/
domain?: string;
/**
* The transient attribute of the session cookie. When true, the cookie will not persist beyond the current session.
*/
transient?: boolean;
}

export interface SessionConfiguration {
Expand Down Expand Up @@ -107,7 +117,9 @@ export abstract class AbstractSessionStore {
httpOnly: true,
sameSite: cookieOptions?.sameSite ?? "lax",
secure: cookieOptions?.secure ?? false,
path: cookieOptions?.path ?? "/"
path: cookieOptions?.path ?? "/",
domain: cookieOptions?.domain,
transient: cookieOptions?.transient
};
}

Expand Down