@auth0/auth0-server-js 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,7 +53,7 @@ The `AUTH0_REDIRECT_URI` is needed to tell Auth0 what URL to redirect back to af
53
53
 
54
54
  ### 3. Configuring the Store
55
55
 
56
- The `auth0-server-js` SDK does not come with a built-in store for both transaction and state data, **it's required to provide a persistent solution** that fits your use-case.
56
+ The `auth0-server-js` SDK comes with a built-in store for both transaction and state data, however **it's required to provide it a CookieHandler implementation** that fits your use-case.
57
57
  The goal of `auth0-server-js` is to provide a flexible API that allows you to use any storage mechanism you prefer, but is mostly designed to work with cookie and session-based storage kept in mind.
58
58
 
59
59
  The SDK methods accept an optional `storeOptions` object that can be used to pass additional options to the storage methods, such as Request / Response objects, allowing to control cookies in the storage layer.
@@ -72,7 +72,7 @@ import { FastifyReply, FastifyRequest } from 'fastify';
72
72
  import { CookieSerializeOptions } from '@fastify/cookie';
73
73
  import {
74
74
  AbstractStateStore,
75
- TransactionStore,
75
+ AbstractTransactionStore,
76
76
  ServerClient,
77
77
  StateData,
78
78
  TransactionData
@@ -83,105 +83,37 @@ export interface StoreOptions {
83
83
  reply: FastifyReply;
84
84
  }
85
85
 
86
- const auth0 = new ServerClient<StoreOptions>({
87
- transactionStore: new StatelessTransactionStore({ secret: options.secret }),
88
- stateStore: new StatelessStateStore({ secret: options.secret }),
89
- });
90
-
91
- export class StatelessTransactionStore implements TransactionStore<StoreOptions> {
92
- async set(identifier: string, transactionData: TransactionData, removeIfExists?: boolean, options?: StoreOptions): Promise<void> {
93
- // We can not handle cookies in Fastify when the `StoreOptions` are not provided.
94
- if (!options) {
95
- throw new Error();
96
- }
97
-
98
- // Note that `removeIfExists` is not used in Stateless storage, but it's kept for compatibility with Stateful storage.
99
-
100
- const maxAge = 60 * 60;
101
- const cookieOpts: CookieSerializeOptions = { httpOnly: true, sameSite: 'lax', path: '/', maxAge };
102
-
103
- options.reply.setCookie(identifier, JSON.stringify(transactionData), cookieOpts);
104
- }
105
-
106
- async get(identifier: string, options?: StoreOptions): Promise<TransactionData | undefined> {
107
- // We can not handle cookies in Fastify when the `StoreOptions` are not provided.
108
- if (!options) {
109
- throw new Error();
110
- }
111
-
112
- const cookieValue = options.request.cookies[identifier];
113
-
114
- if (cookieValue) {
115
- return JSON.parse(cookieValue) as TransactionData;
116
- }
117
- }
118
-
119
- async delete(identifier: string, options?: StoreOptions | undefined): Promise<void> {
120
- // We can not handle cookies in Fastify when the `StoreOptions` are not provided.
121
- if (!options) {
122
- throw new Error();
123
- }
124
-
125
- options?.reply.clearCookie(identifier);
86
+ export class FastifyCookieHandler implements CookieHandler<StoreOptions> {
87
+ setCookie(
88
+ name: string,
89
+ value: string,
90
+ options?: CookieSerializeOptions,
91
+ storeOptions?: StoreOptions
92
+ ): void {
93
+ // Handle storeOptions being undefined if needed.
94
+ storeOptions!.reply.setCookie(name, value, options || {});
126
95
  }
127
- }
128
96
 
129
- export class StatelessStateStore extends AbstractStateStore<StoreOptions> {
130
- async set(
131
- identifier: string,
132
- stateData: StateData,
133
- removeIfExists?: boolean,
134
- options?: StoreOptions | undefined
135
- ): Promise<void> {
136
- // We can not handle cookies in Fastify when the `StoreOptions` are not provided.
137
- if (!options) {
138
- throw new Error();
139
- }
140
-
141
- // Note that `removeIfExists` is not used in Stateless storage, but it's kept for compatibility with Stateful storage.
142
-
143
- const maxAge = ?; // Set the max age of the cookie
144
- const cookieOpts: CookieSerializeOptions = {
145
- httpOnly: true,
146
- sameSite: 'lax',
147
- path: '/',
148
- secure: 'auto',
149
- maxAge,
150
- };
151
- const expiration = Math.floor(Date.now() / 1000 + maxAge);
152
- const encryptedStateData = await this.encrypt(identifier, stateData, expiration);
153
-
154
- options.reply.setCookie(identifier, encryptedStateData, cookieOpts);
97
+ getCookie(name: string, storeOptions?: StoreOptions): string | undefined {
98
+ // Handle storeOptions being undefined if needed.
99
+ return storeOptions!.request.cookies?.[name];
155
100
  }
156
101
 
157
- async get(identifier: string, options?: StoreOptions | undefined): Promise<StateData | undefined> {
158
- // We can not handle cookies in Fastify when the `StoreOptions` are not provided.
159
- if (!options) {
160
- throw new Error();
161
- }
162
-
163
- const encryptedStateData = options.request.cookies[identifier];
164
-
165
- if (encryptedStateData) {
166
- return (await this.decrypt(identifier, encryptedStateData)) as StateData;
167
- }
102
+ getCookies(storeOptions?: StoreOptions): Record<string, string> {
103
+ // Handle storeOptions being undefined if needed.
104
+ return storeOptions!.request.cookies as Record<string, string>;
168
105
  }
169
106
 
170
- async delete(identifier: string, options?: StoreOptions | undefined): Promise<void> {
171
- // We can not handle cookies in Fastify when the `StoreOptions` are not provided.
172
- if (!options) {
173
- throw new Error();
174
- }
175
-
176
- options?.reply.clearCookie(identifier);
177
- }
178
-
179
- deleteByLogoutToken(): Promise<void> {
180
- throw new Error(
181
- 'Backchannel logout is not available when using Stateless Storage. Use Stateful Storage instead.'
182
- );
107
+ deleteCookie(name: string, storeOptions?: StoreOptions): void {
108
+ // Handle storeOptions being undefined if needed.
109
+ storeOptions!.reply.clearCookie(name);
183
110
  }
184
111
  }
112
+
113
+ const auth0 = new ServerClient<StoreOptions>({
114
+ transactionStore: new CookieTransactionStore({ secret: options.secret }, new FastifyCookieHandler()),
115
+ stateStore: new StatelessStateStore({ secret: options.secret }, new FastifyCookieHandler()),
116
+ });
185
117
  ```
186
118
 
187
119
  #### Stateful Store
@@ -207,113 +139,41 @@ export interface StoreOptions {
207
139
  reply: FastifyReply;
208
140
  }
209
141
 
210
- const auth0 = new ServerClient<StoreOptions>({
211
- transactionStore: new StatelessTransactionStore({ secret: '<secret>' }),
212
- stateStore: new StatefulStateStore({ secret: '<secret>' }),
213
- });
214
-
215
- export class StatefulStateStore extends AbstractSessionStore {
216
- async set(
217
- identifier: string,
218
- stateData: StateData,
219
- removeIfExists?: boolean,
220
- options?: StoreOptions | undefined
221
- ): Promise<void> {
222
- // We can not handle cookies in Fastify when the `StoreOptions` are not provided.
223
- if (!options) {
224
- throw new Error();
225
- }
226
-
227
- let sessionId = await this.getSessionId(identifier, options);
228
-
229
- // If this is a new session created by a new login we need to remove the old session
230
- // from the store and regenerate the session ID to prevent session fixation.
231
- if (sessionId && removeIfExists) {
232
- // Delete the session from the store by the sessionId.
233
- // await yourDeleteSessionLogic(sessionId);
234
- sessionId = generateId();
235
- }
236
-
237
- if (!sessionId) {
238
- sessionId = generateId();
239
- }
240
-
241
- const maxAge = ?; // Set the max age of the cookie
242
- const cookieOpts: CookieSerializeOptions = {
243
- httpOnly: true,
244
- sameSite: 'lax',
245
- path: '/',
246
- secure: 'auto',
247
- maxAge,
248
- };
249
- const expiration = Date.now() / 1000 + maxAge;
250
- const encryptedStateData = await this.encrypt<{ id: string }>(
251
- identifier,
252
- {
253
- id: sessionId,
254
- },
255
- expiration
256
- );
257
-
258
- // Save the stateData in the store, identified by the sessionId.
259
- // await yourSaveSessionLogic(sessionId, stateData);
260
-
261
- options.reply.setCookie(identifier, encryptedStateData, cookieOpts);
262
- }
263
-
264
- async get(identifier: string, options?: StoreOptions | undefined): Promise<StateData | undefined> {
265
- // We can not handle cookies in Fastify when the `StoreOptions` are not provided.
266
- if (!options) {
267
- throw new Error();
268
- }
269
-
270
- const sessionId = await this.getSessionId(identifier, options);
271
-
272
- if (sessionId) {
273
- // Retrieve the stateData from the store, identified by the sessionId.
274
- // const stateData = await yourGetSessionLogic(sessionId);
275
-
276
- // If we have a session cookie, but no `stateData`, we should remove the cookie.
277
- if (!stateData) {
278
- options?.reply.clearCookie(identifier);
279
- }
280
-
281
- return stateData;
282
- }
142
+ export class FastifyCookieHandler implements CookieHandler<StoreOptions> {
143
+ setCookie(
144
+ name: string,
145
+ value: string,
146
+ options?: CookieSerializeOptions,
147
+ storeOptions?: StoreOptions,
148
+ ): void {
149
+ // Handle storeOptions being undefined if needed.
150
+ storeOptions!.reply.setCookie(name, value, options || {});
283
151
  }
284
152
 
285
- async delete(identifier: string, options?: StoreOptions | undefined): Promise<void> {
286
- // We can not handle cookies in Fastify when the `StoreOptions` are not provided.
287
- if (!options) {
288
- throw new Error();
289
- }
290
-
291
- const sessionId = await this.getSessionId(identifier, options);
292
-
293
- if (sessionId) {
294
- // Delete the session from the store by the sessionId.
295
- // await yourDeleteSessionLogic(sessionId);
296
- }
297
-
298
- options?.reply.clearCookie(identifier);
153
+ getCookie(name: string, storeOptions?: StoreOptions): string | undefined {
154
+ // Handle storeOptions being undefined if needed.
155
+ return storeOptions!.request.cookies?.[name];
299
156
  }
300
157
 
301
- private async getSessionId(identifier: string, options: StoreOptions) {
302
- const cookieValue = options.request.cookies[identifier];
303
- if (cookieValue) {
304
- const sessionCookie = await this.decrypt<{ id: string }>(identifier, cookieValue);
305
- return sessionCookie.id;
306
- }
158
+ getCookies(storeOptions?: StoreOptions): Record<string, string> {
159
+ // Handle storeOptions being undefined if needed.
160
+ return storeOptions!.request.cookies as Record<string, string>;
307
161
  }
308
162
 
309
- deleteByLogoutToken(claims: LogoutTokenClaims, options?: StoreOptions | undefined): Promise<void> {
310
- // Delete the session from the store by the LogoutTokenClaims (sub and sid)
311
- // await yourDeleteSessionByLogoutTokenLogic(sessionId);
163
+ deleteCookie(name: string, storeOptions?: StoreOptions): void {
164
+ // Handle storeOptions being undefined if needed.
165
+ storeOptions!.reply.clearCookie(name);
312
166
  }
313
167
  }
168
+
169
+ const auth0 = new ServerClient<StoreOptions>({
170
+ transactionStore: new CookieTransactionStore({ secret: options.secret }, new FastifyCookieHandler()),
171
+ stateStore: new StatefulStateStore({ secret: options.secret }, new FastifyCookieHandler()),
172
+ });
173
+
314
174
  ```
315
175
 
316
- Note that `storeOptions` is optional, but required when wanting to interact with the framework to set cookies. Here's how to pass the `storeOptions` to `startInteractiveLogin()` in a Fastify application:
176
+ Note that `storeOptions` is optional in the SDK's methods, but required when wanting to interact with the framework to set cookies. Here's how to pass the `storeOptions` to `startInteractiveLogin()` in a Fastify application:
317
177
 
318
178
  ```ts
319
179
  fastify.get('/auth/login', async (request, reply) => {
@@ -324,7 +184,7 @@ fastify.get('/auth/login', async (request, reply) => {
324
184
  });
325
185
  ```
326
186
 
327
- Because storage systems in Web Applications are mostly cookie-based, the `storeOptions` object is used to pass the `request` and `reply` objects to the storage methods, allowing to control cookies in the storage layer. It's expected to pass this to every interaction with the SDK.
187
+ Because storage systems in Web Applications are mostly cookie-based, the `storeOptions` object is used to pass the `request` and `reply` (in the case of Fastify, as per the example) objects to the storage methods, allowing to control cookies in the storage layer. It's expected to pass this to every interaction with the SDK.
328
188
 
329
189
  ### 4. Add login to your Application (interactive)
330
190
 
package/dist/index.cjs CHANGED
@@ -23,11 +23,14 @@ __export(index_exports, {
23
23
  AbstractStateStore: () => AbstractStateStore,
24
24
  AbstractTransactionStore: () => AbstractTransactionStore,
25
25
  BackchannelLogoutError: () => BackchannelLogoutError,
26
+ CookieTransactionStore: () => CookieTransactionStore,
26
27
  MissingRequiredArgumentError: () => MissingRequiredArgumentError,
27
28
  MissingSessionError: () => MissingSessionError,
28
29
  MissingTransactionError: () => MissingTransactionError,
29
30
  ServerClient: () => ServerClient,
30
- StartLinkUserError: () => StartLinkUserError
31
+ StartLinkUserError: () => StartLinkUserError,
32
+ StatefulStateStore: () => StatefulStateStore,
33
+ StatelessStateStore: () => StatelessStateStore
31
34
  });
32
35
  module.exports = __toCommonJS(index_exports);
33
36
 
@@ -544,15 +547,198 @@ var AbstractTransactionStore = class extends AbstractStore {
544
547
  super(options);
545
548
  }
546
549
  };
550
+
551
+ // src/store/cookie-transaction-store.ts
552
+ var CookieTransactionStore = class extends AbstractTransactionStore {
553
+ #cookieHandler;
554
+ constructor(options, cookieHandler) {
555
+ super(options);
556
+ this.#cookieHandler = cookieHandler;
557
+ }
558
+ async set(identifier, transactionData, removeIfExists, options) {
559
+ const maxAge = 60 * 60;
560
+ const cookieOpts = { httpOnly: true, sameSite: "lax", path: "/", maxAge };
561
+ const expiration = Math.floor(Date.now() / 1e3 + maxAge);
562
+ const encryptedStateData = await this.encrypt(identifier, transactionData, expiration);
563
+ this.#cookieHandler.setCookie(identifier, encryptedStateData, cookieOpts, options);
564
+ }
565
+ async get(identifier, options) {
566
+ const cookieValue = this.#cookieHandler.getCookie(identifier, options);
567
+ if (cookieValue) {
568
+ return await this.decrypt(identifier, cookieValue);
569
+ }
570
+ }
571
+ async delete(identifier, options) {
572
+ this.#cookieHandler.deleteCookie(identifier, options);
573
+ }
574
+ };
575
+
576
+ // src/store/abstract-session-store.ts
577
+ var AbstractSessionStore = class extends AbstractStateStore {
578
+ #rolling;
579
+ #absoluteDuration;
580
+ #inactivityDuration;
581
+ constructor(options) {
582
+ super(options);
583
+ this.#rolling = options.rolling ?? true;
584
+ this.#absoluteDuration = options.absoluteDuration ?? 60 * 60 * 24 * 3;
585
+ this.#inactivityDuration = options.inactivityDuration ?? 60 * 60 * 24 * 1;
586
+ }
587
+ /**
588
+ * calculateMaxAge calculates the max age of the session based on createdAt and the rolling and absolute durations.
589
+ */
590
+ calculateMaxAge(createdAt) {
591
+ if (!this.#rolling) {
592
+ return this.#absoluteDuration;
593
+ }
594
+ const now = Date.now() / 1e3 | 0;
595
+ const expiresAt = Math.min(now + this.#inactivityDuration, createdAt + this.#absoluteDuration);
596
+ const maxAge = expiresAt - now;
597
+ return maxAge > 0 ? maxAge : 0;
598
+ }
599
+ };
600
+
601
+ // src/store/stateful-state-store.ts
602
+ var generateId = () => {
603
+ const bytes = new Uint8Array(16);
604
+ crypto.getRandomValues(bytes);
605
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
606
+ };
607
+ var StatefulStateStore = class extends AbstractSessionStore {
608
+ #store;
609
+ #cookieOptions;
610
+ #cookieHandler;
611
+ constructor(options, cookieHandler) {
612
+ super(options);
613
+ this.#store = options.store;
614
+ this.#cookieOptions = options.cookie;
615
+ this.#cookieHandler = cookieHandler;
616
+ }
617
+ async set(identifier, stateData, removeIfExists, options) {
618
+ let sessionId = await this.getSessionId(identifier, options);
619
+ if (sessionId && removeIfExists) {
620
+ await this.#store.delete(sessionId);
621
+ sessionId = generateId();
622
+ }
623
+ sessionId ??= generateId();
624
+ const maxAge = this.calculateMaxAge(stateData.internal.createdAt);
625
+ const cookieOpts = {
626
+ httpOnly: true,
627
+ sameSite: this.#cookieOptions?.sameSite ?? "lax",
628
+ path: "/",
629
+ secure: this.#cookieOptions?.secure,
630
+ maxAge
631
+ };
632
+ const expiration = Date.now() / 1e3 + maxAge;
633
+ const encryptedStateData = await this.encrypt(
634
+ identifier,
635
+ {
636
+ id: sessionId
637
+ },
638
+ expiration
639
+ );
640
+ await this.#store.set(sessionId, stateData);
641
+ this.#cookieHandler.setCookie(identifier, encryptedStateData, cookieOpts, options);
642
+ }
643
+ async get(identifier, options) {
644
+ const sessionId = await this.getSessionId(identifier, options);
645
+ if (sessionId) {
646
+ const stateData = await this.#store.get(sessionId);
647
+ if (!stateData) {
648
+ this.#cookieHandler.deleteCookie(identifier, options);
649
+ }
650
+ return stateData;
651
+ }
652
+ }
653
+ async delete(identifier, options) {
654
+ const sessionId = await this.getSessionId(identifier, options);
655
+ if (sessionId) {
656
+ await this.#store.delete(sessionId);
657
+ }
658
+ this.#cookieHandler.deleteCookie(identifier, options);
659
+ }
660
+ async getSessionId(identifier, options) {
661
+ const cookieValue = this.#cookieHandler.getCookie(identifier, options);
662
+ if (cookieValue) {
663
+ const sessionCookie = await this.decrypt(identifier, cookieValue);
664
+ return sessionCookie.id;
665
+ }
666
+ }
667
+ deleteByLogoutToken(claims, options) {
668
+ return this.#store.deleteByLogoutToken(claims, options);
669
+ }
670
+ };
671
+
672
+ // src/store/stateless-state-store.ts
673
+ var StatelessStateStore = class extends AbstractSessionStore {
674
+ #cookieHandler;
675
+ constructor(options, cookieHandler) {
676
+ super(options);
677
+ this.#cookieHandler = cookieHandler;
678
+ }
679
+ async set(identifier, stateData, removeIfExists, options) {
680
+ const maxAge = this.calculateMaxAge(stateData.internal.createdAt);
681
+ const cookieOpts = {
682
+ httpOnly: true,
683
+ sameSite: "lax",
684
+ path: "/",
685
+ secure: true,
686
+ maxAge
687
+ };
688
+ const expiration = Math.floor(Date.now() / 1e3 + maxAge);
689
+ const encryptedStateData = await this.encrypt(identifier, stateData, expiration);
690
+ const chunkSize = 3072;
691
+ const chunkCount = Math.ceil(encryptedStateData.length / chunkSize);
692
+ const chunks = [...Array(chunkCount).keys()].map((i) => ({
693
+ value: encryptedStateData.substring(i * chunkSize, (i + 1) * chunkSize),
694
+ name: `${identifier}.${i}`
695
+ }));
696
+ chunks.forEach((chunk) => {
697
+ this.#cookieHandler.setCookie(chunk.name, chunk.value, cookieOpts, options);
698
+ });
699
+ const existingCookieKeys = this.getCookieKeys(identifier, options);
700
+ const cookieKeysToRemove = existingCookieKeys.filter((key) => !chunks.some((chunk) => chunk.name === key));
701
+ cookieKeysToRemove.forEach((key) => {
702
+ this.#cookieHandler.deleteCookie(key, options);
703
+ });
704
+ }
705
+ async get(identifier, options) {
706
+ const cookieKeys = this.getCookieKeys(identifier, options);
707
+ const encryptedStateData = cookieKeys.map((key) => ({
708
+ index: parseInt(key.split(".")[1], 10),
709
+ value: this.#cookieHandler.getCookie(key, options)
710
+ })).sort((a, b) => a.index - b.index).map((item) => item.value).join("");
711
+ if (encryptedStateData) {
712
+ return await this.decrypt(identifier, encryptedStateData);
713
+ }
714
+ }
715
+ async delete(identifier, options) {
716
+ const cookieKeys = this.getCookieKeys(identifier, options);
717
+ for (const key of cookieKeys) {
718
+ this.#cookieHandler.deleteCookie(key, options);
719
+ }
720
+ }
721
+ deleteByLogoutToken() {
722
+ throw new Error(
723
+ "Backchannel logout is not available when using Stateless Storage. Use Stateful Storage by providing a `sessionStore`"
724
+ );
725
+ }
726
+ getCookieKeys(identifier, options) {
727
+ return Object.keys(this.#cookieHandler.getCookies(options)).filter((key) => key.startsWith(identifier));
728
+ }
729
+ };
547
730
  // Annotate the CommonJS export names for ESM import in node:
548
731
  0 && (module.exports = {
549
732
  AbstractStateStore,
550
733
  AbstractTransactionStore,
551
734
  BackchannelLogoutError,
735
+ CookieTransactionStore,
552
736
  MissingRequiredArgumentError,
553
737
  MissingSessionError,
554
738
  MissingTransactionError,
555
739
  ServerClient,
556
- StartLinkUserError
740
+ StartLinkUserError,
741
+ StatefulStateStore,
742
+ StatelessStateStore
557
743
  });
558
744
  //# sourceMappingURL=index.cjs.map