@auth0/auth0-server-js 1.0.0

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 ADDED
@@ -0,0 +1,428 @@
1
+ The `@auth0/auth0-server-js` library allows for implementing user authentication in web applications on a JavaScript runtime.
2
+
3
+ Using this SDK as-is in your application may not be trivial, as it is designed to be used as a building block for building framework-specific authentication SDKs.
4
+
5
+ ![Release](https://img.shields.io/npm/v/@auth0/auth0-server-js)
6
+ ![Downloads](https://img.shields.io/npm/dw/@auth0/auth0-server-js)
7
+ [![License](https://img.shields.io/:license-mit-blue.svg?style=flat)](https://opensource.org/licenses/MIT)
8
+
9
+ 📚 [Documentation](#documentation) - 🚀 [Getting Started](#getting-started) - 💬 [Feedback](#feedback)
10
+
11
+ ## Documentation
12
+
13
+ - [Examples](https://github.com/auth0/auth0-server-js/blob/main/packages/auth0-server-js/EXAMPLES.md) - examples for your different use cases.
14
+ - [Docs Site](https://auth0.com/docs) - explore our docs site and learn more about Auth0.
15
+
16
+ ## Getting Started
17
+
18
+ - [1. Install the SDK](#1-install-the-sdk)
19
+ - [2. Create the Auth0 SDK client](#2-create-the-auth0-sdk-client)
20
+ - [3. Configuring the Store](#3-configuring-the-store)
21
+ - [Stateless Store](#stateless-store)
22
+ - [Stateful Store](#stateful-store)
23
+ - [4. Add login to your Application (interactive)](#4-add-login-to-your-application-interactive)
24
+ - [5. Add logout to your application](#5-add-logout-to-your-application)
25
+
26
+ ### 1. Install the SDK
27
+
28
+ ```shell
29
+ npm i @auth0/auth0-server-js
30
+ ```
31
+
32
+ This library requires Node.js 20 LTS and newer LTS versions.
33
+
34
+ ### 2. Create the Auth0 SDK client
35
+
36
+ Create an instance of the `ServerClient`. This instance will be imported and used anywhere we need access to the authentication methods.
37
+
38
+ ```ts
39
+ import { ServerClient } from '@auth0/auth0-server-js';
40
+
41
+ const auth0 = new ServerClient<StoreOptions>({
42
+ domain: '<AUTH0_DOMAIN>',
43
+ clientId: '<AUTH0_CLIENT_ID>',
44
+ clientSecret: '<AUTH0_CLIENT_SECRET>',
45
+ authorizationParams: {
46
+ redirect_uri: '<AUTH0_REDIRECT_URI>',
47
+ },
48
+ });
49
+ ```
50
+
51
+ The `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, and `AUTH0_CLIENT_SECRET` can be obtained from the [Auth0 Dashboard](https://manage.auth0.com) once you've created an application. **This application must be a `Regular Web Application`**.
52
+ The `AUTH0_REDIRECT_URI` is needed to tell Auth0 what URL to redirect back to after successfull authentication, e.g. `http://localhost:3000/auth/callback`. (note, your application needs to handle this endpoint and call the SDK's `completeInteractiveLogin(url: string)` to finish the authentication process. See below for more information)
53
+
54
+ ### 3. Configuring the Store
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.
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
+
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.
60
+
61
+ For Web Applications, this may come down to a Stateless or Statefull session storage system.
62
+
63
+ #### Stateless Store
64
+
65
+ In a stateless storage solution, the entire session data is stored in the cookie. This is the simplest form of storage, but it has some limitations, such as the maximum size of a cookie.
66
+
67
+ The implementation may vary depending on the framework of choice, here is an example using Fastify:
68
+
69
+
70
+ ```ts
71
+ import { FastifyReply, FastifyRequest } from 'fastify';
72
+ import { CookieSerializeOptions } from '@fastify/cookie';
73
+ import {
74
+ AbstractStateStore,
75
+ TransactionStore,
76
+ ServerClient,
77
+ StateData,
78
+ TransactionData
79
+ } from '@auth0/auth0-server-js';
80
+
81
+ export interface StoreOptions {
82
+ request: FastifyRequest;
83
+ reply: FastifyReply;
84
+ }
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);
126
+ }
127
+ }
128
+
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);
155
+ }
156
+
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
+ }
168
+ }
169
+
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
+ );
183
+ }
184
+ }
185
+ ```
186
+
187
+ #### Stateful Store
188
+
189
+ In stateful storage, the session data is stored in a server-side storage mechanism, such as a database. This allows for more flexibility in the size of the session data, but requires additional infrastructure to manage the storage.
190
+ The session is identified by a unique identifier that is stored in the cookie, which the storage would read in order to retrieve the session data from the server-side storage.
191
+
192
+
193
+ The implementation may vary depending on the framework of choice, here is an example using Fastify:
194
+
195
+ ```ts
196
+ import type { FastifyReply, FastifyRequest } from "fastify";
197
+ import { CookieSerializeOptions } from '@fastify/cookie';
198
+ import {
199
+ AbstractStateStore,
200
+ LogoutTokenClaims,
201
+ ServerClient,
202
+ StateData,
203
+ } from '@auth0/auth0-server-js';
204
+
205
+ export interface StoreOptions {
206
+ request: FastifyRequest;
207
+ reply: FastifyReply;
208
+ }
209
+
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
+ }
283
+ }
284
+
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);
299
+ }
300
+
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
+ }
307
+ }
308
+
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);
312
+ }
313
+ }
314
+ ```
315
+
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:
317
+
318
+ ```ts
319
+ fastify.get('/auth/login', async (request, reply) => {
320
+ const storeOptions = { request, reply };
321
+ const authorizationUrl = await auth0Client.startInteractiveLogin({}, storeOptions);
322
+
323
+ reply.redirect(authorizationUrl.href);
324
+ });
325
+ ```
326
+
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.
328
+
329
+ ### 4. Add login to your Application (interactive)
330
+
331
+ Before using redirect-based login, ensure the `authorizationParams.redirect_uri` is configured when initializing the SDK:
332
+
333
+ ```ts
334
+ const auth0 = new ServerClient<StoreOptions>({
335
+ // ...
336
+ authorizationParams: {
337
+ redirect_uri: '<AUTH0_REDIRECT_URI>',
338
+ },
339
+ // ...
340
+ });
341
+ ```
342
+
343
+ > [!IMPORTANT]
344
+ > You will need to register the `AUTH0_REDIRECT_URI` in your Auth0 Application as an **Allowed Callback URLs** via the [Auth0 Dashboard](https://manage.auth0.com):
345
+
346
+ In order to add login to any application, call `startInteractiveLogin()`, and redirect the user to the returned URL.
347
+
348
+ The implementation will vary based on the framework being used, but here is an example of what this would look like in Fastify:
349
+
350
+ ```ts
351
+ fastify.get('/auth/login', async (request, reply) => {
352
+ const authorizationUrl = await auth0Client.startInteractiveLogin({
353
+ // The redirect_uri can also be configured here.
354
+ authorizationParams: {
355
+ redirect_uri: '<AUTH0_REDIRECT_URI>',
356
+ },
357
+ }, { request, reply });
358
+
359
+ reply.redirect(authorizationUrl.href);
360
+ });
361
+ ```
362
+
363
+ Once the user has succesfully authenticated, Auth0 will redirect the user back to the provided `authorizationParams.redirect_uri` which needs to be handled in the application.
364
+ The implementation will vary based on the framework used, but what needs to happen is:
365
+
366
+ - register an endpoint that will handle the configured `authorizationParams.redirect_uri`.
367
+ - call the SDK's `completeInteractiveLogin(url)`, passing it the full URL, including query parameters.
368
+
369
+ Here is an example of what this would look like in Fastify, with `authorizationParams.redirect_uri` configured as `http://localhost:3000/auth/callback`:
370
+
371
+ ```ts
372
+ fastify.get('/auth/callback', async (request, reply) => {
373
+ await auth0Client.completeInteractiveLogin(new URL(request.url, options.appBaseUrl), { request, reply });
374
+
375
+ reply.redirect('/');
376
+ });
377
+ ```
378
+
379
+ ### 5. Add logout to your application
380
+
381
+ In order to log the user out of your application, as well as from Auth0, you can call the SDK's `logout()` method, and redirect the user to the returned URL.
382
+
383
+ ```ts
384
+ fastify.get('/auth/logout', async (request, reply) => {
385
+ const logoutUrl = await auth0Client.logout({ returnTo: '<RETURN_TO>' }, { request, reply });
386
+
387
+ reply.redirect(logoutUrl.href);
388
+ });
389
+ ```
390
+
391
+ > [!IMPORTANT]
392
+ > You will need to register the `RETURN_TO` in your Auth0 Application as an **Allowed Logout URLs** via the [Auth0 Dashboard](https://manage.auth0.com):
393
+
394
+
395
+
396
+ ## Feedback
397
+
398
+ ### Contributing
399
+
400
+ We appreciate feedback and contribution to this repo! Before you get started, please read the following:
401
+
402
+ - [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
403
+ - [Auth0's code of conduct guidelines](https://github.com/auth0/auth0-server-js/blob/main/CODE-OF-CONDUCT.md)
404
+ - [This repo's contribution guide](./../../CONTRIBUTING.md)
405
+
406
+ ### Raise an issue
407
+
408
+ To provide feedback or report a bug, please [raise an issue on our issue tracker](https://github.com/auth0/auth0-server-js/issues).
409
+
410
+ ## Vulnerability Reporting
411
+
412
+ Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues.
413
+
414
+ ## What is Auth0?
415
+
416
+ <p align="center">
417
+ <picture>
418
+ <source media="(prefers-color-scheme: dark)" srcset="https://cdn.auth0.com/website/sdks/logos/auth0_dark_mode.png" width="150">
419
+ <source media="(prefers-color-scheme: light)" srcset="https://cdn.auth0.com/website/sdks/logos/auth0_light_mode.png" width="150">
420
+ <img alt="Auth0 Logo" src="https://cdn.auth0.com/website/sdks/logos/auth0_light_mode.png" width="150">
421
+ </picture>
422
+ </p>
423
+ <p align="center">
424
+ Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout <a href="https://auth0.com/why-auth0">Why Auth0?</a>
425
+ </p>
426
+ <p align="center">
427
+ This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-server-js/blob/main/packages/auth0-auth-js/LICENSE"> LICENSE</a> file for more info.
428
+ </p>