Skip to content

Commit 1078139

Browse files
guabunandan-bhat
authored andcommitted
feat: allow configuring cookie name and samesite attribute (#1872)
1 parent 35ce37d commit 1078139

9 files changed

+355
-29
lines changed

src/server/client.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import { RequestCookies, ResponseCookies } from "./cookies"
1616
import {
1717
AbstractSessionStore,
1818
SessionConfiguration,
19+
SessionCookieOptions,
1920
} from "./session/abstract-session-store"
2021
import { StatefulSessionStore } from "./session/stateful-session-store"
2122
import { StatelessSessionStore } from "./session/stateless-session-store"
22-
import { TransactionStore } from "./transaction-store"
23+
import { TransactionCookieOptions, TransactionStore } from "./transaction-store"
2324

2425
interface Auth0ClientOptions {
2526
// authorization server configuration
@@ -88,6 +89,12 @@ interface Auth0ClientOptions {
8889
*/
8990
session?: SessionConfiguration
9091

92+
// transaction cookie configuration
93+
/**
94+
* Configure the transaction cookie used to store the state of the authentication transaction.
95+
*/
96+
transactionCookie?: TransactionCookieOptions
97+
9198
// hooks
9299
/**
93100
* A method to manipulate the session before persisting it.
@@ -162,33 +169,43 @@ export class Auth0Client {
162169
options.clientAssertionSigningAlg ||
163170
process.env.AUTH0_CLIENT_ASSERTION_SIGNING_ALG
164171

165-
const cookieOptions = {
166-
secure: false,
172+
const sessionCookieOptions: SessionCookieOptions = {
173+
name: options.session?.cookie?.name ?? "__session",
174+
secure: options.session?.cookie?.secure ?? false,
175+
sameSite: options.session?.cookie?.sameSite ?? "lax",
167176
}
177+
178+
const transactionCookieOptions: TransactionCookieOptions = {
179+
prefix: options.transactionCookie?.prefix ?? "__txn_",
180+
secure: options.transactionCookie?.secure ?? false,
181+
sameSite: options.transactionCookie?.sameSite ?? "lax",
182+
}
183+
168184
if (appBaseUrl) {
169185
const { protocol } = new URL(appBaseUrl)
170186
if (protocol === "https:") {
171-
cookieOptions.secure = true
187+
sessionCookieOptions.secure = true
188+
transactionCookieOptions.secure = true
172189
}
173190
}
174191

175192
this.transactionStore = new TransactionStore({
176193
...options.session,
177194
secret,
178-
cookieOptions,
195+
cookieOptions: transactionCookieOptions,
179196
})
180197

181198
this.sessionStore = options.sessionStore
182199
? new StatefulSessionStore({
183200
...options.session,
184201
secret,
185202
store: options.sessionStore,
186-
cookieOptions,
203+
cookieOptions: sessionCookieOptions,
187204
})
188205
: new StatelessSessionStore({
189206
...options.session,
190207
secret,
191-
cookieOptions,
208+
cookieOptions: sessionCookieOptions,
192209
})
193210

194211
this.authClient = new AuthClient({

src/server/cookies.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export async function decrypt<T>(cookieValue: string, secret: string) {
4040

4141
export interface CookieOptions {
4242
httpOnly: boolean
43-
sameSite: "lax" | "strict"
43+
sameSite: "lax" | "strict" | "none"
4444
secure: boolean
4545
path: string
4646
maxAge?: number

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

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@ import {
66
ResponseCookies,
77
} from "../cookies"
88

9+
export interface SessionCookieOptions {
10+
/**
11+
* The name of the session cookie.
12+
*
13+
* Default: `__session`.
14+
*/
15+
name?: string
16+
/**
17+
* The sameSite attribute of the session cookie.
18+
*
19+
* Default: `lax`.
20+
*/
21+
sameSite?: "strict" | "lax" | "none"
22+
/**
23+
* The secure attribute of the session cookie.
24+
*
25+
* Default: depends on the protocol of the application's base URL. If the protocol is `https`, then `true`, otherwise `false`.
26+
*/
27+
secure?: boolean
28+
}
29+
930
export interface SessionConfiguration {
1031
/**
1132
* A boolean indicating whether rolling sessions should be used or not.
@@ -32,18 +53,25 @@ export interface SessionConfiguration {
3253
* Default: 1 day.
3354
*/
3455
inactivityDuration?: number
56+
57+
/**
58+
* The options for the session cookie.
59+
*/
60+
cookie?: SessionCookieOptions
3561
}
3662

3763
interface SessionStoreOptions extends SessionConfiguration {
3864
secret: string
3965
store?: SessionDataStore
4066

41-
cookieOptions?: Partial<Pick<CookieOptions, "secure">>
67+
cookieOptions?: SessionCookieOptions
4268
}
4369

70+
const SESSION_COOKIE_NAME = "__session"
71+
4472
export abstract class AbstractSessionStore {
4573
public secret: string
46-
public SESSION_COOKIE_NAME = "__session"
74+
public sessionCookieName: string
4775

4876
private rolling: boolean
4977
private absoluteDuration: number
@@ -70,9 +98,10 @@ export abstract class AbstractSessionStore {
7098
this.inactivityDuration = inactivityDuration
7199
this.store = store
72100

101+
this.sessionCookieName = cookieOptions?.name ?? SESSION_COOKIE_NAME
73102
this.cookieConfig = {
74103
httpOnly: true,
75-
sameSite: "lax",
104+
sameSite: cookieOptions?.sameSite ?? "lax",
76105
secure: cookieOptions?.secure ?? false,
77106
path: "/",
78107
}

src/server/session/stateful-session-store.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,106 @@ describe("Stateful Session Store", async () => {
378378
expect(cookie?.maxAge).toEqual(1800)
379379
expect(cookie?.secure).toEqual(true)
380380
})
381+
382+
it("should apply the sameSite attribute to the cookie", async () => {
383+
const currentTime = Date.now()
384+
const createdAt = Math.floor(currentTime / 1000)
385+
const secret = await generateSecret(32)
386+
const session: SessionData = {
387+
user: { sub: "user_123" },
388+
tokenSet: {
389+
accessToken: "at_123",
390+
refreshToken: "rt_123",
391+
expiresAt: 123456,
392+
},
393+
internal: {
394+
sid: "auth0-sid",
395+
createdAt,
396+
},
397+
}
398+
const store = {
399+
get: vi.fn().mockResolvedValue(session),
400+
set: vi.fn(),
401+
delete: vi.fn(),
402+
}
403+
404+
const requestCookies = new RequestCookies(new Headers())
405+
const responseCookies = new ResponseCookies(new Headers())
406+
407+
const sessionStore = new StatefulSessionStore({
408+
secret,
409+
store,
410+
rolling: true,
411+
absoluteDuration: 3600,
412+
inactivityDuration: 1800,
413+
414+
cookieOptions: {
415+
sameSite: "strict",
416+
},
417+
})
418+
await sessionStore.set(requestCookies, responseCookies, session)
419+
420+
const cookie = responseCookies.get("__session")
421+
const cookieValue = await decrypt(cookie!.value, secret)
422+
423+
expect(cookie).toBeDefined()
424+
expect(cookieValue).toHaveProperty("id")
425+
expect(cookie?.path).toEqual("/")
426+
expect(cookie?.httpOnly).toEqual(true)
427+
expect(cookie?.sameSite).toEqual("strict")
428+
expect(cookie?.maxAge).toEqual(1800)
429+
expect(cookie?.secure).toEqual(false)
430+
})
431+
432+
it("should apply the cookie name", async () => {
433+
const currentTime = Date.now()
434+
const createdAt = Math.floor(currentTime / 1000)
435+
const secret = await generateSecret(32)
436+
const session: SessionData = {
437+
user: { sub: "user_123" },
438+
tokenSet: {
439+
accessToken: "at_123",
440+
refreshToken: "rt_123",
441+
expiresAt: 123456,
442+
},
443+
internal: {
444+
sid: "auth0-sid",
445+
createdAt,
446+
},
447+
}
448+
const store = {
449+
get: vi.fn().mockResolvedValue(session),
450+
set: vi.fn(),
451+
delete: vi.fn(),
452+
}
453+
454+
const requestCookies = new RequestCookies(new Headers())
455+
const responseCookies = new ResponseCookies(new Headers())
456+
457+
const sessionStore = new StatefulSessionStore({
458+
secret,
459+
store,
460+
rolling: true,
461+
absoluteDuration: 3600,
462+
inactivityDuration: 1800,
463+
464+
cookieOptions: {
465+
name: "my-session",
466+
},
467+
})
468+
await sessionStore.set(requestCookies, responseCookies, session)
469+
470+
const cookie = responseCookies.get("my-session")
471+
const cookieValue = await decrypt(cookie!.value, secret)
472+
473+
expect(cookie).toBeDefined()
474+
expect(cookieValue).toHaveProperty("id")
475+
expect(cookie?.path).toEqual("/")
476+
expect(cookie?.httpOnly).toEqual(true)
477+
expect(cookie?.sameSite).toEqual("lax")
478+
expect(cookie?.maxAge).toEqual(1800)
479+
expect(cookie?.secure).toEqual(false)
480+
})
381481
})
382482
})
383483

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { SessionData, SessionDataStore } from "../../types"
22
import * as cookies from "../cookies"
3-
import { AbstractSessionStore } from "./abstract-session-store"
3+
import {
4+
AbstractSessionStore,
5+
SessionCookieOptions,
6+
} from "./abstract-session-store"
47

58
// the value of the stateful session cookie containing a unique session ID to identify
69
// the current session
@@ -17,7 +20,7 @@ interface StatefulSessionStoreOptions {
1720

1821
store: SessionDataStore
1922

20-
cookieOptions?: Partial<Pick<cookies.CookieOptions, "secure">>
23+
cookieOptions?: SessionCookieOptions
2124
}
2225

2326
const generateId = () => {
@@ -51,7 +54,7 @@ export class StatefulSessionStore extends AbstractSessionStore {
5154
}
5255

5356
async get(reqCookies: cookies.RequestCookies) {
54-
const cookieValue = reqCookies.get(this.SESSION_COOKIE_NAME)?.value
57+
const cookieValue = reqCookies.get(this.sessionCookieName)?.value
5558

5659
if (!cookieValue) {
5760
return null
@@ -73,7 +76,7 @@ export class StatefulSessionStore extends AbstractSessionStore {
7376
) {
7477
// check if a session already exists. If so, maintain the existing session ID
7578
let sessionId = null
76-
const cookieValue = reqCookies.get(this.SESSION_COOKIE_NAME)?.value
79+
const cookieValue = reqCookies.get(this.sessionCookieName)?.value
7780
if (cookieValue) {
7881
const sessionCookie = await cookies.decrypt<SessionCookieValue>(
7982
cookieValue,
@@ -101,22 +104,22 @@ export class StatefulSessionStore extends AbstractSessionStore {
101104
)
102105
const maxAge = this.calculateMaxAge(session.internal.createdAt)
103106

104-
resCookies.set(this.SESSION_COOKIE_NAME, jwe.toString(), {
107+
resCookies.set(this.sessionCookieName, jwe.toString(), {
105108
...this.cookieConfig,
106109
maxAge,
107110
})
108111
await this.store.set(sessionId, session)
109112

110113
// to enable read-after-write in the same request for middleware
111-
reqCookies.set(this.SESSION_COOKIE_NAME, jwe.toString())
114+
reqCookies.set(this.sessionCookieName, jwe.toString())
112115
}
113116

114117
async delete(
115118
reqCookies: cookies.RequestCookies,
116119
resCookies: cookies.ResponseCookies
117120
) {
118-
const cookieValue = reqCookies.get(this.SESSION_COOKIE_NAME)?.value
119-
await resCookies.delete(this.SESSION_COOKIE_NAME)
121+
const cookieValue = reqCookies.get(this.sessionCookieName)?.value
122+
await resCookies.delete(this.sessionCookieName)
120123

121124
if (!cookieValue) {
122125
return

0 commit comments

Comments
 (0)