Skip to content

Commit 3abe778

Browse files
added cookie config options
1 parent dbfd502 commit 3abe778

File tree

5 files changed

+253
-28
lines changed

5 files changed

+253
-28
lines changed

Diff for: src/server/chunked-cookies.test.ts

+193-1
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,204 @@ describe("Chunked Cookie Utils", () => {
231231
// It is called 3 times.
232232
// 2 times for the chunks
233233
// 1 time for the non chunked cookie
234-
expect(reqCookies.delete).toHaveBeenCalledTimes(3);
234+
expect(reqCookies.delete).toHaveBeenCalledTimes(3);
235235
expect(reqCookies.delete).toHaveBeenCalledWith(`${name}__3`);
236236
expect(reqCookies.delete).toHaveBeenCalledWith(`${name}__4`);
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);

Diff for: src/server/client.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,22 @@ 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+
httpOnly:
210+
options.session?.cookie?.httpOnly ??
211+
process.env.AUTH0_COOKIE_HTTP_ONLY !== "false",
212+
transient:
213+
options.session?.cookie?.transient ??
214+
process.env.AUTH0_COOKIE_TRANSIENT === "true",
215+
domain: options.session?.cookie?.domain ?? process.env.AUTH0_COOKIE_DOMAIN
203216
};
204217

205218
const transactionCookieOptions: TransactionCookieOptions = {

Diff for: src/server/cookies.ts

+15-6
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,16 +180,23 @@ 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

189198
// When we are writing a non-chunked cookie, we should remove the chunked cookies
190-
getAllChunkedCookies(reqCookies, name).forEach(cookieChunk => {
199+
getAllChunkedCookies(reqCookies, name).forEach((cookieChunk) => {
191200
resCookies.delete(cookieChunk.name);
192201
reqCookies.delete(cookieChunk.name);
193202
});
@@ -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;
@@ -223,9 +232,9 @@ export function setChunkedCookie(
223232
}
224233
}
225234

226-
// When we have written chunked cookies, we should remove the non-chunked cookie
227-
resCookies.delete(name);
228-
reqCookies.delete(name);
235+
// When we have written chunked cookies, we should remove the non-chunked cookie
236+
resCookies.delete(name);
237+
reqCookies.delete(name);
229238
}
230239

231240
/**

Diff for: src/server/session/abstract-session-store.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ 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 httpOnly attribute of the session cookie. When true, the cookie is not accessible via JavaScript.
40+
*/
41+
httpOnly?: boolean;
42+
/**
43+
* The transient attribute of the session cookie. When true, the cookie will not persist beyond the current session.
44+
*/
45+
transient?: boolean;
3246
}
3347

3448
export interface SessionConfiguration {
@@ -107,7 +121,9 @@ export abstract class AbstractSessionStore {
107121
httpOnly: true,
108122
sameSite: cookieOptions?.sameSite ?? "lax",
109123
secure: cookieOptions?.secure ?? false,
110-
path: cookieOptions?.path ?? "/"
124+
path: cookieOptions?.path ?? "/",
125+
domain: cookieOptions?.domain,
126+
transient: cookieOptions?.transient
111127
};
112128
}
113129

Diff for: src/server/session/stateless-session-store.ts

+12-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { CookieOptions, ConnectionTokenSet, SessionData } from "../../types";
2-
31
import type { JWTPayload } from "jose";
42

3+
import { ConnectionTokenSet, CookieOptions, SessionData } from "../../types";
54
import * as cookies from "../cookies";
65
import {
76
AbstractSessionStore,
@@ -55,25 +54,26 @@ export class StatelessSessionStore extends AbstractSessionStore {
5554
SessionData | LegacySessionPayload
5655
>(cookieValue, this.secret);
5756

58-
const normalizedStatelessSession = normalizeStatelessSession(originalSession);
57+
const normalizedStatelessSession =
58+
normalizeStatelessSession(originalSession);
5959

6060
// As connection access tokens are stored in seperate cookies,
6161
// we need to get all cookies and only use those that are prefixed with `this.connectionTokenSetsCookieName`
6262
const connectionTokenSets = await Promise.all(
63-
this.getConnectionTokenSetsCookies(reqCookies).map(
64-
(cookie) =>
65-
cookies.decrypt<ConnectionTokenSet>(
66-
cookie.value,
67-
this.secret
68-
)
63+
this.getConnectionTokenSetsCookies(reqCookies).map((cookie) =>
64+
cookies.decrypt<ConnectionTokenSet>(cookie.value, this.secret)
6965
)
7066
);
7167

7268
return {
7369
...normalizedStatelessSession,
7470
// Ensure that when there are no connection token sets, we omit the property.
7571
...(connectionTokenSets.length
76-
? { connectionTokenSets: connectionTokenSets.map(tokenSet => tokenSet.payload) }
72+
? {
73+
connectionTokenSets: connectionTokenSets.map(
74+
(tokenSet) => tokenSet.payload
75+
)
76+
}
7777
: {})
7878
};
7979
}
@@ -117,7 +117,7 @@ export class StatelessSessionStore extends AbstractSessionStore {
117117
)
118118
);
119119
}
120-
120+
121121
// Any existing v3 cookie can be deleted as soon as we have set a v4 cookie.
122122
// In stateless sessions, we do have to ensure we delete all chunks.
123123
cookies.deleteChunkedCookie(LEGACY_COOKIE_NAME, reqCookies, resCookies);
@@ -127,11 +127,7 @@ export class StatelessSessionStore extends AbstractSessionStore {
127127
reqCookies: cookies.RequestCookies,
128128
resCookies: cookies.ResponseCookies
129129
) {
130-
cookies.deleteChunkedCookie(
131-
this.sessionCookieName,
132-
reqCookies,
133-
resCookies
134-
);
130+
cookies.deleteChunkedCookie(this.sessionCookieName, reqCookies, resCookies);
135131

136132
this.getConnectionTokenSetsCookies(reqCookies).forEach((cookie) =>
137133
resCookies.delete(cookie.name)
@@ -177,7 +173,6 @@ export class StatelessSessionStore extends AbstractSessionStore {
177173
"You can use a stateful session implementation to store the session data in a data store."
178174
);
179175
}
180-
181176
}
182177
}
183178

0 commit comments

Comments
 (0)