Skip to content

Commit f1eb46d

Browse files
Extensive Cookie Configuration (#2059)
2 parents da33ec8 + 6dec246 commit f1eb46d

File tree

6 files changed

+303
-7
lines changed

6 files changed

+303
-7
lines changed

EXAMPLES.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- [`beforeSessionSaved`](#beforesessionsaved)
2525
- [`onCallback`](#oncallback)
2626
- [Session configuration](#session-configuration)
27+
- [Cookie Configuration](#cookie-configuration)
2728
- [Database sessions](#database-sessions)
2829
- [Back-Channel Logout](#back-channel-logout)
2930
- [Combining middleware](#combining-middleware)
@@ -685,6 +686,67 @@ export const auth0 = new Auth0Client({
685686
| absoluteDuration | `number` | The absolute duration after which the session will expire. The value must be specified in seconds. Default: `3 days`. |
686687
| inactivityDuration | `number` | The duration of inactivity after which the session will expire. The value must be specified in seconds. Default: `1 day`. |
687688

689+
## Cookie Configuration
690+
691+
You can configure the session cookie attributes either through environment variables or directly in the SDK initialization.
692+
693+
**1. Using Environment Variables:**
694+
695+
Set the desired environment variables in your `.env.local` file or your deployment environment:
696+
697+
```
698+
# .env.local
699+
# ... other variables ...
700+
701+
# Cookie Options
702+
AUTH0_COOKIE_DOMAIN='.example.com' # Set cookie for subdomains
703+
AUTH0_COOKIE_PATH='/app' # Limit cookie to /app path
704+
AUTH0_COOKIE_TRANSIENT=true # Make cookie transient (session-only)
705+
AUTH0_COOKIE_SECURE=true # Recommended for production
706+
AUTH0_COOKIE_SAME_SITE='Lax'
707+
```
708+
709+
The SDK will automatically pick up these values. Note that `httpOnly` is always set to `true` for security reasons and cannot be configured.
710+
711+
**2. Using `Auth0ClientOptions`:**
712+
713+
Configure the options directly when initializing the client:
714+
715+
```typescript
716+
import { Auth0Client } from "@auth0/nextjs-auth0/server"
717+
718+
export const auth0 = new Auth0Client({
719+
session: {
720+
cookie: {
721+
domain: '.example.com',
722+
path: '/app',
723+
transient: true,
724+
// httpOnly is always true and cannot be configured
725+
secure: process.env.NODE_ENV === 'production',
726+
sameSite: 'Lax',
727+
// name: 'appSession', // Optional: custom cookie name, defaults to '__session'
728+
},
729+
// ... other session options like absoluteDuration ...
730+
},
731+
// ... other client options ...
732+
});
733+
```
734+
735+
**Session Cookie Options:**
736+
737+
* `domain` (String): Specifies the `Domain` attribute.
738+
* `path` (String): Specifies the `Path` attribute. Defaults to `/`.
739+
* `transient` (Boolean): If `true`, the `maxAge` attribute is omitted, making it a session cookie. Defaults to `false`.
740+
* `secure` (Boolean): Specifies the `Secure` attribute. Defaults to `false` (or `true` if `AUTH0_COOKIE_SECURE=true` is set).
741+
* `sameSite` ('Lax' | 'Strict' | 'None'): Specifies the `SameSite` attribute. Defaults to `Lax` (or the value of `AUTH0_COOKIE_SAME_SITE`).
742+
* `name` (String): The name of the session cookie. Defaults to `__session`.
743+
744+
> [!INFO]
745+
> Options provided directly in `Auth0ClientOptions` take precedence over environment variables. The `httpOnly` attribute is always `true` regardless of configuration.
746+
747+
> [!INFO]
748+
> The `httpOnly` attribute for the session cookie is always set to `true` for security reasons and cannot be configured via options or environment variables.
749+
688750
## Database sessions
689751

690752
By default, the user's sessions are stored in encrypted cookies. You may choose to persist the sessions in your data store of choice.

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ You can customize the client by using the options below:
137137
| 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. |
138138
| 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. |
139139
| signInReturnToPath | `string` | The path to redirect the user to after successfully authenticating. Defaults to `/`. |
140-
| 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. |
140+
| 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. |
141141
| 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. |
142142
| 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. |
143143
| 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. |
@@ -147,6 +147,17 @@ You can customize the client by using the options below:
147147
| httpTimeout | `number` | Integer value for the HTTP timeout in milliseconds for authentication requests. Defaults to `5000` milliseconds |
148148
| 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`. |
149149

150+
## Session Cookie Configuration
151+
You can specify the following environment variables to configure the session cookie:
152+
```env
153+
AUTH0_COOKIE_DOMAIN=
154+
AUTH0_COOKIE_PATH=
155+
AUTH0_COOKIE_TRANSIENT=
156+
AUTH0_COOKIE_SECURE=
157+
AUTH0_COOKIE_SAME_SITE=
158+
```
159+
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.
160+
150161
## Configuration Validation
151162

152163
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:

src/server/chunked-cookies.test.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,198 @@ describe("Chunked Cookie Utils", () => {
237237
expect(reqCookies.delete).toHaveBeenCalledWith(name);
238238
});
239239

240+
// New tests for domain and transient options
241+
it("should set the domain property for a single cookie", () => {
242+
const name = "domainCookie";
243+
const value = "small value";
244+
const options: CookieOptions = {
245+
path: "/",
246+
domain: "example.com",
247+
httpOnly: true,
248+
secure: true,
249+
sameSite: "lax"
250+
};
251+
252+
setChunkedCookie(name, value, options, reqCookies, resCookies);
253+
254+
expect(resCookies.set).toHaveBeenCalledTimes(1);
255+
expect(resCookies.set).toHaveBeenCalledWith(
256+
name,
257+
value,
258+
expect.objectContaining({ domain: "example.com" })
259+
);
260+
});
261+
262+
it("should set the domain property for chunked cookies", () => {
263+
const name = "largeDomainCookie";
264+
const largeValue = "a".repeat(8000);
265+
const options: CookieOptions = {
266+
path: "/",
267+
domain: "example.com",
268+
httpOnly: true,
269+
secure: true,
270+
sameSite: "lax"
271+
};
272+
273+
setChunkedCookie(name, largeValue, options, reqCookies, resCookies);
274+
275+
expect(resCookies.set).toHaveBeenCalledTimes(3); // 3 chunks
276+
expect(resCookies.set).toHaveBeenNthCalledWith(
277+
1,
278+
`${name}__0`,
279+
expect.any(String),
280+
expect.objectContaining({ domain: "example.com" })
281+
);
282+
expect(resCookies.set).toHaveBeenNthCalledWith(
283+
2,
284+
`${name}__1`,
285+
expect.any(String),
286+
expect.objectContaining({ domain: "example.com" })
287+
);
288+
expect(resCookies.set).toHaveBeenNthCalledWith(
289+
3,
290+
`${name}__2`,
291+
expect.any(String),
292+
expect.objectContaining({ domain: "example.com" })
293+
);
294+
});
295+
296+
it("should omit maxAge for a single transient cookie", () => {
297+
const name = "transientCookie";
298+
const value = "small value";
299+
const options: CookieOptions = {
300+
path: "/",
301+
maxAge: 3600,
302+
transient: true,
303+
httpOnly: true,
304+
secure: true,
305+
sameSite: "lax"
306+
};
307+
const expectedOptions = { ...options };
308+
delete expectedOptions.maxAge; // maxAge should be removed
309+
delete expectedOptions.transient; // transient flag itself is not part of the cookie options
310+
311+
setChunkedCookie(name, value, options, reqCookies, resCookies);
312+
313+
expect(resCookies.set).toHaveBeenCalledTimes(1);
314+
expect(resCookies.set).toHaveBeenCalledWith(name, value, expectedOptions);
315+
expect(resCookies.set).not.toHaveBeenCalledWith(
316+
name,
317+
value,
318+
expect.objectContaining({ maxAge: 3600 })
319+
);
320+
});
321+
322+
it("should omit maxAge for chunked transient cookies", () => {
323+
const name = "largeTransientCookie";
324+
const largeValue = "a".repeat(8000);
325+
const options: CookieOptions = {
326+
path: "/",
327+
maxAge: 3600,
328+
transient: true,
329+
httpOnly: true,
330+
secure: true,
331+
sameSite: "lax"
332+
};
333+
const expectedOptions = { ...options };
334+
delete expectedOptions.maxAge; // maxAge should be removed
335+
delete expectedOptions.transient; // transient flag itself is not part of the cookie options
336+
337+
setChunkedCookie(name, largeValue, options, reqCookies, resCookies);
338+
339+
expect(resCookies.set).toHaveBeenCalledTimes(3); // 3 chunks
340+
expect(resCookies.set).toHaveBeenNthCalledWith(
341+
1,
342+
`${name}__0`,
343+
expect.any(String),
344+
expectedOptions
345+
);
346+
expect(resCookies.set).toHaveBeenNthCalledWith(
347+
2,
348+
`${name}__1`,
349+
expect.any(String),
350+
expectedOptions
351+
);
352+
expect(resCookies.set).toHaveBeenNthCalledWith(
353+
3,
354+
`${name}__2`,
355+
expect.any(String),
356+
expectedOptions
357+
);
358+
expect(resCookies.set).not.toHaveBeenCalledWith(
359+
expect.any(String),
360+
expect.any(String),
361+
expect.objectContaining({ maxAge: 3600 })
362+
);
363+
});
364+
365+
it("should include maxAge for a single non-transient cookie", () => {
366+
const name = "nonTransientCookie";
367+
const value = "small value";
368+
const options: CookieOptions = {
369+
path: "/",
370+
maxAge: 3600,
371+
transient: false,
372+
httpOnly: true,
373+
secure: true,
374+
sameSite: "lax"
375+
};
376+
const expectedOptions = { ...options };
377+
delete expectedOptions.transient; // transient flag itself is not part of the cookie options
378+
379+
setChunkedCookie(name, value, options, reqCookies, resCookies);
380+
381+
expect(resCookies.set).toHaveBeenCalledTimes(1);
382+
expect(resCookies.set).toHaveBeenCalledWith(name, value, expectedOptions);
383+
expect(resCookies.set).toHaveBeenCalledWith(
384+
name,
385+
value,
386+
expect.objectContaining({ maxAge: 3600 })
387+
);
388+
});
389+
390+
it("should include maxAge for chunked non-transient cookies", () => {
391+
const name = "largeNonTransientCookie";
392+
const largeValue = "a".repeat(8000);
393+
const options: CookieOptions = {
394+
path: "/",
395+
maxAge: 3600,
396+
transient: false,
397+
httpOnly: true,
398+
secure: true,
399+
sameSite: "lax"
400+
};
401+
const expectedOptions = { ...options };
402+
delete expectedOptions.transient; // transient flag itself is not part of the cookie options
403+
404+
setChunkedCookie(name, largeValue, options, reqCookies, resCookies);
405+
406+
expect(resCookies.set).toHaveBeenCalledTimes(3); // 3 chunks
407+
expect(resCookies.set).toHaveBeenNthCalledWith(
408+
1,
409+
`${name}__0`,
410+
expect.any(String),
411+
expectedOptions
412+
);
413+
expect(resCookies.set).toHaveBeenNthCalledWith(
414+
2,
415+
`${name}__1`,
416+
expect.any(String),
417+
expectedOptions
418+
);
419+
expect(resCookies.set).toHaveBeenNthCalledWith(
420+
3,
421+
`${name}__2`,
422+
expect.any(String),
423+
expectedOptions
424+
);
425+
expect(resCookies.set).toHaveBeenCalledWith(
426+
expect.any(String),
427+
expect.any(String),
428+
expect.objectContaining({ maxAge: 3600 })
429+
);
430+
});
431+
240432
describe("getChunkedCookie", () => {
241433
it("should return undefined when cookie does not exist", () => {
242434
const result = getChunkedCookie("nonexistent", reqCookies);

src/server/client.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,19 @@ export class Auth0Client {
197197

198198
const sessionCookieOptions: SessionCookieOptions = {
199199
name: options.session?.cookie?.name ?? "__session",
200-
secure: options.session?.cookie?.secure ?? false,
201-
sameSite: options.session?.cookie?.sameSite ?? "lax",
202-
path: options.session?.cookie?.path ?? "/"
200+
secure:
201+
options.session?.cookie?.secure ??
202+
process.env.AUTH0_COOKIE_SECURE === "true",
203+
sameSite:
204+
options.session?.cookie?.sameSite ??
205+
(process.env.AUTH0_COOKIE_SAME_SITE as "lax" | "strict" | "none") ??
206+
"lax",
207+
path:
208+
options.session?.cookie?.path ?? process.env.AUTH0_COOKIE_PATH ?? "/",
209+
transient:
210+
options.session?.cookie?.transient ??
211+
process.env.AUTH0_COOKIE_TRANSIENT === "true",
212+
domain: options.session?.cookie?.domain ?? process.env.AUTH0_COOKIE_DOMAIN
203213
};
204214

205215
const transactionCookieOptions: TransactionCookieOptions = {

src/server/cookies.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ export interface CookieOptions {
113113
secure: boolean;
114114
path: string;
115115
maxAge?: number;
116+
domain?: string;
117+
transient?: boolean;
116118
}
117119

118120
export type ReadonlyRequestCookies = Omit<
@@ -178,11 +180,18 @@ export function setChunkedCookie(
178180
reqCookies: RequestCookies,
179181
resCookies: ResponseCookies
180182
): void {
183+
const { transient, ...restOptions } = options;
184+
const finalOptions = { ...restOptions };
185+
186+
if (transient) {
187+
delete finalOptions.maxAge;
188+
}
189+
181190
const valueBytes = new TextEncoder().encode(value).length;
182191

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

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

206-
resCookies.set(chunkName, chunk, options);
215+
resCookies.set(chunkName, chunk, finalOptions);
207216
// to enable read-after-write in the same request for middleware
208217
reqCookies.set(chunkName, chunk);
209218
position += MAX_CHUNK_SIZE;

src/server/session/abstract-session-store.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ export interface SessionCookieOptions {
2929
* The path attribute of the session cookie. Will be set to '/' by default.
3030
*/
3131
path?: string;
32+
/**
33+
* Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.3|Domain Set-Cookie attribute}. By default, no
34+
* domain is set, and most clients will consider the cookie to apply to only
35+
* the current domain.
36+
*/
37+
domain?: string;
38+
/**
39+
* The transient attribute of the session cookie. When true, the cookie will not persist beyond the current session.
40+
*/
41+
transient?: boolean;
3242
}
3343

3444
export interface SessionConfiguration {
@@ -107,7 +117,9 @@ export abstract class AbstractSessionStore {
107117
httpOnly: true,
108118
sameSite: cookieOptions?.sameSite ?? "lax",
109119
secure: cookieOptions?.secure ?? false,
110-
path: cookieOptions?.path ?? "/"
120+
path: cookieOptions?.path ?? "/",
121+
domain: cookieOptions?.domain,
122+
transient: cookieOptions?.transient
111123
};
112124
}
113125

0 commit comments

Comments
 (0)