@react-foundry/fastify-auth 0.1.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (C) 2019-2025 Crown Copyright
4
+ Copyright (C) 2019-2026 Daniel A.C. Martin
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
7
+ this software and associated documentation files (the "Software"), to deal in
8
+ the Software without restriction, including without limitation the rights to
9
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10
+ of the Software, and to permit persons to whom the Software is furnished to do
11
+ so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ React Foundry - Fastify-Auth
2
+ ============================
3
+
4
+ Authentication plugin for Fastify. Allows you to authenticate uses by a variety
5
+ of methods and obtain identity information and roles for use on each request.
6
+ Also provides a session when requested or when required by the authentication
7
+ method.
8
+
9
+
10
+ Using this package
11
+ ------------------
12
+
13
+ First install the package into your project:
14
+
15
+ ```shell
16
+ npm install -S @react-foundry/fastify-auth
17
+ ```
18
+
19
+ Then use it in your code as follows:
20
+
21
+ ```ts
22
+ import Fastify from 'fastify';
23
+ import { AuthMethod, fastifyAuth } from '@react-foundry/fastify-auth';
24
+
25
+ const httpd = Fastify();
26
+
27
+ httpd.register(fastifyAuth, {
28
+ privacy: false,
29
+ session: {
30
+ cookies: {
31
+ secret: 'changeme' // Change this to a secret string that is shared across instances of your application
32
+ }
33
+ },
34
+ method: AuthMethod.OIDC,
35
+ issuer: 'https://keycloak/realms/my-realm/', // Change this to the URL for your OIDC provider
36
+ clientId: 'my-client', // Change this to your client ID
37
+ clientSecret: 'my-client-secret', // Change this to your client secret
38
+ redirectUri: 'https://my-website/' // Change this to the base URL that the OIDC provider should redirect back to
39
+ });
40
+
41
+ httpd.get('/', async (req, _reply) => {
42
+ const user = req.user.username || 'guest';
43
+ const message = `Hello ${user}`;
44
+
45
+ return message;
46
+ });
47
+
48
+ await httpd.listen({
49
+ host: '::'
50
+ port: 8080
51
+ });
52
+ ```
53
+
54
+
55
+ ### `fastifyAuth`
56
+
57
+ A Fastify plugin which can be 'registered' with the following options.
58
+
59
+ Options object:
60
+
61
+ - **`method: 'none' | 'dummy' | 'headers' | 'basic' | 'oidc'`**
62
+ The method to use to authenticate users.
63
+ - `'none'`: No authentication (you might want this if you just want a
64
+ session).
65
+ - `'dummy'`: Dummy authentication; useful for testing.
66
+ - `'headers'`: Trusts auth information provided on HTTP headers (such as by a
67
+ reverse proxy). **WARNING:** This is insecure when not behind a
68
+ reverse authentication proxy.
69
+ - `'basic'`: HTTP [Basic authentication].
70
+ - `'oidc'`: [OpenID Connect]. (Such as [Keycloak] etc.)
71
+ - **`privacy: boolean`**
72
+ When `true`, user must be authenticated to access any part of the website.
73
+ When `false`, users can still access the website but may not have access to
74
+ all features when those features require certain roles. Users can typically
75
+ choose to log-in to acquire extra permissions.
76
+ - **`session: object`**
77
+ See: https://www.npmjs.com/package/@react-foundry/fastify-session
78
+
79
+
80
+ ### `req.user: object`
81
+
82
+ An object representing the user data. Includes the `username` and `roles`. When
83
+ using OIDC, it should also contain the `accessToken` for making requests to
84
+ other services on behalf of the user.
85
+
86
+
87
+ Working on this package
88
+ -----------------------
89
+
90
+ Before working on this package you must install its dependencies using
91
+ the following command:
92
+
93
+ ```shell
94
+ pnpm install
95
+ ```
96
+
97
+
98
+ ### Building
99
+
100
+ Build the package by compiling the source code.
101
+
102
+ ```shell
103
+ npm run build
104
+ ```
105
+
106
+
107
+ ### Clean-up
108
+
109
+ Remove any previously built files.
110
+
111
+ ```shell
112
+ npm run clean
113
+ ```
114
+
115
+ [Basic authentication]: https://en.wikipedia.org/wiki/Basic_access_authentication
116
+ [OpenID Connect]: https://en.wikipedia.org/wiki/OpenID#OpenID_Connect_(OIDC)
117
+ [Keycloak]: https://www.keycloak.org/
@@ -0,0 +1,10 @@
1
+ import { AuthBagger } from './common';
2
+ export type Options = {
3
+ username: string;
4
+ password: string;
5
+ roles?: string[];
6
+ realm?: string;
7
+ charset?: BufferEncoding;
8
+ };
9
+ export declare const basic: AuthBagger<Options>;
10
+ export default basic;
package/dist/basic.js ADDED
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.basic = void 0;
4
+ const error_1 = require("@fastify/error");
5
+ const authPrefix = 'Basic ';
6
+ const BadHeader = (0, error_1.createError)('FST_BAD_HEADER', 'Malformed Authorization header', 400);
7
+ const AuthFailed = (0, error_1.createError)('FST_BASIC_AUTH_FAILED', 'Incorrect or missing authentication details', 401);
8
+ const basic = ({ password, roles = [], username, realm = 'members', charset = 'utf-8' }, _fullSessions) => {
9
+ const base64Decode = (s) => Buffer.from(s, 'base64').toString(charset);
10
+ const decodeHeader = (s) => {
11
+ try {
12
+ return base64Decode(s.substring(authPrefix.length)).split(':');
13
+ }
14
+ catch (_err) {
15
+ throw new BadHeader();
16
+ }
17
+ };
18
+ return {
19
+ authenticate: (req, reply) => {
20
+ const authHeader = req.headers['authorization'] || '';
21
+ const [suppliedUsername, suppliedPassword] = (authHeader.startsWith(authPrefix)
22
+ ? decodeHeader(authHeader)
23
+ : []);
24
+ if (suppliedUsername === username && suppliedPassword === password) {
25
+ const user = {
26
+ username,
27
+ roles
28
+ };
29
+ req.user = user;
30
+ }
31
+ else {
32
+ reply.header('WWW-Authenticate', `Basic realm="${realm}", charset="${charset}"`);
33
+ throw new AuthFailed();
34
+ }
35
+ },
36
+ wantSession: false
37
+ };
38
+ };
39
+ exports.basic = basic;
40
+ exports.default = exports.basic;
package/dist/basic.mjs ADDED
@@ -0,0 +1,36 @@
1
+ import { createError } from '@fastify/error';
2
+ const authPrefix = 'Basic ';
3
+ const BadHeader = createError('FST_BAD_HEADER', 'Malformed Authorization header', 400);
4
+ const AuthFailed = createError('FST_BASIC_AUTH_FAILED', 'Incorrect or missing authentication details', 401);
5
+ export const basic = ({ password, roles = [], username, realm = 'members', charset = 'utf-8' }, _fullSessions) => {
6
+ const base64Decode = (s) => Buffer.from(s, 'base64').toString(charset);
7
+ const decodeHeader = (s) => {
8
+ try {
9
+ return base64Decode(s.substring(authPrefix.length)).split(':');
10
+ }
11
+ catch (_err) {
12
+ throw new BadHeader();
13
+ }
14
+ };
15
+ return {
16
+ authenticate: (req, reply) => {
17
+ const authHeader = req.headers['authorization'] || '';
18
+ const [suppliedUsername, suppliedPassword] = (authHeader.startsWith(authPrefix)
19
+ ? decodeHeader(authHeader)
20
+ : []);
21
+ if (suppliedUsername === username && suppliedPassword === password) {
22
+ const user = {
23
+ username,
24
+ roles
25
+ };
26
+ req.user = user;
27
+ }
28
+ else {
29
+ reply.header('WWW-Authenticate', `Basic realm="${realm}", charset="${charset}"`);
30
+ throw new AuthFailed();
31
+ }
32
+ },
33
+ wantSession: false
34
+ };
35
+ };
36
+ export default basic;
@@ -0,0 +1,48 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { Request as _Request, RequestFull as _RequestFull, Reply, ReplyFull } from '@react-foundry/fastify-session';
3
+ export type Promised<T> = T | Promise<T>;
4
+ export type Maybe<T> = T | undefined;
5
+ export type UserProfile = {
6
+ provider?: string;
7
+ id?: string;
8
+ displayName?: string;
9
+ name?: {
10
+ familyName?: string;
11
+ givenName?: string;
12
+ middleName?: string;
13
+ };
14
+ emails?: Array<{
15
+ value: string;
16
+ type?: string;
17
+ }>;
18
+ photos?: Array<{
19
+ value: string;
20
+ }>;
21
+ username: string;
22
+ groups?: string[];
23
+ roles: string[];
24
+ expiry?: Date;
25
+ };
26
+ type RequestExtras = {
27
+ user?: UserProfile;
28
+ };
29
+ export type Request = _Request & Partial<RequestExtras>;
30
+ export type RequestFull = _RequestFull & RequestExtras;
31
+ export type SubRouteHandlerMethod = (request: RequestFull, reply: Reply) => Promised<unknown | void>;
32
+ export type RouteHandlerMethod = (this: FastifyInstance, request: Request, reply: Reply) => Promised<unknown | void>;
33
+ type UserExtractor = (req: _Request) => Maybe<UserProfile>;
34
+ export declare const fromExtractor: (extractor: UserExtractor) => SubRouteHandlerMethod;
35
+ export type Serialize<A extends UserProfile = UserProfile, B = Partial<A>> = (user: A, req: Request) => Promised<B>;
36
+ export type Deserialize<A extends UserProfile = UserProfile, B = Partial<A>> = (data: B, req: Request) => Promised<Maybe<A>>;
37
+ export type SerDes<T = UserProfile> = (data: T, req: Request) => Promised<T>;
38
+ export type AuthBag = {
39
+ authenticate: SubRouteHandlerMethod;
40
+ callback?: SubRouteHandlerMethod;
41
+ serialize?: Serialize;
42
+ deserialize?: Deserialize;
43
+ terminate?: SubRouteHandlerMethod;
44
+ wantSession: boolean;
45
+ };
46
+ export type AuthBagger<T> = (config: T, fullSessions: boolean) => Promised<AuthBag>;
47
+ export declare const id: <T>(x: T) => T;
48
+ export type { Reply, ReplyFull };
package/dist/common.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.id = exports.fromExtractor = void 0;
4
+ const fromExtractor = (extractor) => (req, _reply) => {
5
+ req.user = extractor(req);
6
+ };
7
+ exports.fromExtractor = fromExtractor;
8
+ const id = (x) => x;
9
+ exports.id = id;
@@ -0,0 +1,4 @@
1
+ export const fromExtractor = (extractor) => (req, _reply) => {
2
+ req.user = extractor(req);
3
+ };
4
+ export const id = (x) => x;
@@ -0,0 +1,8 @@
1
+ import type { AuthBagger } from './common';
2
+ export type Options = {
3
+ username: string;
4
+ groups?: string[];
5
+ roles?: string[];
6
+ };
7
+ export declare const dummy: AuthBagger<Options>;
8
+ export default dummy;
package/dist/dummy.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dummy = void 0;
4
+ const common_1 = require("./common");
5
+ const dummy = ({ username, groups = [], roles = [], }, _fullSessions) => ({
6
+ authenticate: (0, common_1.fromExtractor)((_) => ({
7
+ username,
8
+ groups,
9
+ roles
10
+ })),
11
+ wantSession: false
12
+ });
13
+ exports.dummy = dummy;
14
+ exports.default = exports.dummy;
package/dist/dummy.mjs ADDED
@@ -0,0 +1,10 @@
1
+ import { fromExtractor } from './common';
2
+ export const dummy = ({ username, groups = [], roles = [], }, _fullSessions) => ({
3
+ authenticate: fromExtractor((_) => ({
4
+ username,
5
+ groups,
6
+ roles
7
+ })),
8
+ wantSession: false
9
+ });
10
+ export default dummy;
@@ -0,0 +1,8 @@
1
+ import type { AuthBagger } from './common';
2
+ export type Options = {
3
+ usernameHeader?: string;
4
+ groupsHeader?: string;
5
+ rolesHeader?: string;
6
+ };
7
+ export declare const headers: AuthBagger<Options>;
8
+ export default headers;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.headers = void 0;
4
+ const common_1 = require("./common");
5
+ const valueFromHeader = (header) => (Array.isArray(header)
6
+ ? header[0]
7
+ : header);
8
+ const headers = ({ groupsHeader = 'x-auth-groups', rolesHeader = 'x-auth-roles', usernameHeader = 'x-auth-username' }, _fullSessions) => ({
9
+ authenticate: (0, common_1.fromExtractor)((req) => {
10
+ const username = valueFromHeader(req.headers[usernameHeader]);
11
+ const groups = valueFromHeader(req.headers[groupsHeader]);
12
+ const roles = valueFromHeader(req.headers[rolesHeader]);
13
+ return (username && roles
14
+ ? {
15
+ username: username,
16
+ groups: groups?.split(',') || [],
17
+ roles: roles?.split(',') || []
18
+ }
19
+ : undefined);
20
+ }),
21
+ wantSession: false
22
+ });
23
+ exports.headers = headers;
24
+ exports.default = exports.headers;
@@ -0,0 +1,20 @@
1
+ import { fromExtractor } from './common';
2
+ const valueFromHeader = (header) => (Array.isArray(header)
3
+ ? header[0]
4
+ : header);
5
+ export const headers = ({ groupsHeader = 'x-auth-groups', rolesHeader = 'x-auth-roles', usernameHeader = 'x-auth-username' }, _fullSessions) => ({
6
+ authenticate: fromExtractor((req) => {
7
+ const username = valueFromHeader(req.headers[usernameHeader]);
8
+ const groups = valueFromHeader(req.headers[groupsHeader]);
9
+ const roles = valueFromHeader(req.headers[rolesHeader]);
10
+ return (username && roles
11
+ ? {
12
+ username: username,
13
+ groups: groups?.split(',') || [],
14
+ roles: roles?.split(',') || []
15
+ }
16
+ : undefined);
17
+ }),
18
+ wantSession: false
19
+ });
20
+ export default headers;
@@ -0,0 +1,42 @@
1
+ import type { FastifyPluginCallback } from 'fastify';
2
+ import type { RateLimitOptions, RateLimitPluginOptions } from '@fastify/rate-limit';
3
+ import type { FastifySessionOptions } from '@react-foundry/fastify-session';
4
+ import type { Options as _BasicOptions } from './basic';
5
+ import type { Options as _DummyOptions } from './dummy';
6
+ import type { Options as _HeadersOptions } from './headers';
7
+ import type { Options as _OIDCOptions } from './oidc';
8
+ import type { Reply, Request, RequestFull } from './common';
9
+ export declare enum AuthMethod {
10
+ None = "none",
11
+ Dummy = "dummy",
12
+ Headers = "headers",
13
+ Basic = "basic",
14
+ OIDC = "oidc"
15
+ }
16
+ type Method<T> = {
17
+ method: T;
18
+ };
19
+ type NoneOptions = Method<AuthMethod.None> | {
20
+ method?: undefined;
21
+ };
22
+ type DummyOptions = Method<AuthMethod.Dummy> & _DummyOptions;
23
+ type HeadersOptions = Method<AuthMethod.Headers> & _HeadersOptions;
24
+ type BasicOptions = Method<AuthMethod.Basic> & _BasicOptions;
25
+ type OIDCOptions = Method<AuthMethod.OIDC> & _OIDCOptions;
26
+ type MethodOptions = NoneOptions | DummyOptions | HeadersOptions | BasicOptions | OIDCOptions;
27
+ export type FastifyAuthPluginOptions = MethodOptions & {
28
+ privacy?: boolean;
29
+ pathPrefix?: string;
30
+ session?: FastifySessionOptions;
31
+ signInPath?: string;
32
+ signOutPath?: string;
33
+ callbackPath?: string;
34
+ redirectPath?: string;
35
+ rateLimit?: Omit<RateLimitPluginOptions, 'global'>;
36
+ authRateLimit?: RateLimitOptions;
37
+ };
38
+ export declare const fastifyAuth: FastifyPluginCallback<FastifyAuthPluginOptions>;
39
+ export default fastifyAuth;
40
+ export type { FastifyAuthPluginOptions as FastifyAuthOptions, Reply, Request, RequestFull, };
41
+ export type { ReplyFull, RouteHandlerMethod } from './common';
42
+ export { SessionStore } from '@react-foundry/fastify-session';
package/dist/index.js ADDED
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.SessionStore = exports.fastifyAuth = exports.AuthMethod = void 0;
40
+ const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
41
+ const rate_limit_1 = __importDefault(require("@fastify/rate-limit"));
42
+ const fastify_session_1 = __importStar(require("@react-foundry/fastify-session"));
43
+ const basic_1 = require("./basic");
44
+ const dummy_1 = require("./dummy");
45
+ const headers_1 = require("./headers");
46
+ const oidc_1 = require("./oidc");
47
+ var AuthMethod;
48
+ (function (AuthMethod) {
49
+ AuthMethod["None"] = "none";
50
+ AuthMethod["Dummy"] = "dummy";
51
+ AuthMethod["Headers"] = "headers";
52
+ AuthMethod["Basic"] = "basic";
53
+ AuthMethod["OIDC"] = "oidc";
54
+ })(AuthMethod || (exports.AuthMethod = AuthMethod = {}));
55
+ ;
56
+ const isNone = (v) => v.method === AuthMethod.None || v.method === undefined;
57
+ const isDummy = (v) => v.method === AuthMethod.Dummy;
58
+ const isHeaders = (v) => v.method === AuthMethod.Headers;
59
+ const isBasic = (v) => v.method === AuthMethod.Basic;
60
+ const isOIDC = (v) => v.method === AuthMethod.OIDC;
61
+ const fastifyAuthPlugin = async (fastify, { privacy = true, pathPrefix = '/auth/', session = {}, signInPath = 'sign-in', signOutPath = 'sign-out', callbackPath = 'callback', redirectPath = '/', rateLimit: _rateLimit = {
62
+ max: 60,
63
+ timeWindow: 60000,
64
+ }, authRateLimit = {
65
+ max: 100,
66
+ timeWindow: 15 * 60000
67
+ }, ...methodOptions }) => {
68
+ const fullSessions = !!(session.store && session.store !== fastify_session_1.SessionStore.Cookie);
69
+ const serDes = (user, _req) => user;
70
+ const redact = (user, _req) => ({
71
+ username: user.username,
72
+ roles: user.roles
73
+ });
74
+ const _serialize = (fullSessions
75
+ ? serDes
76
+ : redact);
77
+ const redirect = async (_req, reply) => {
78
+ return reply.redirect(redirectPath, 302);
79
+ };
80
+ const rateLimit = _rateLimit && {
81
+ ..._rateLimit,
82
+ global: privacy
83
+ };
84
+ const authConfig = {
85
+ config: {
86
+ rateLimit: authRateLimit
87
+ }
88
+ };
89
+ const { authenticate, callback, deserialize = serDes, serialize = _serialize, terminate, wantSession } = await (isDummy(methodOptions) ? (0, dummy_1.dummy)(methodOptions, fullSessions)
90
+ : isHeaders(methodOptions) ? (0, headers_1.headers)(methodOptions, fullSessions)
91
+ : isBasic(methodOptions) ? (0, basic_1.basic)(methodOptions, fullSessions)
92
+ : isOIDC(methodOptions) ? (0, oidc_1.oidc)(methodOptions, fullSessions)
93
+ : {});
94
+ const useSession = wantSession || !privacy;
95
+ const whitelist = (callback
96
+ ? [pathPrefix + callbackPath, pathPrefix + signOutPath]
97
+ : [pathPrefix + signOutPath]);
98
+ fastify.decorateRequest('user', null);
99
+ if (!fastify.hasDecorator('rateLimit') && (authenticate || callback)) {
100
+ fastify.register(rate_limit_1.default, rateLimit);
101
+ }
102
+ if (authenticate) {
103
+ if (useSession) {
104
+ fastify.addHook('onSend', async (req, _reply, _payload) => {
105
+ if (req.user) {
106
+ if (req.session) {
107
+ req.session.user = await serialize(req.user, req);
108
+ }
109
+ else {
110
+ req.log.error('Unable to store session');
111
+ }
112
+ }
113
+ });
114
+ }
115
+ }
116
+ if (useSession || session.store) {
117
+ if (!session.store) {
118
+ fastify.log.info('Session required for authentication; registering plugin...');
119
+ }
120
+ fastify.register(fastify_session_1.default, session);
121
+ }
122
+ if (authenticate) {
123
+ if (useSession) {
124
+ fastify.addHook('preHandler', async (req, _reply) => {
125
+ if (req.session?.user) {
126
+ req.user = await deserialize(req.session.user, req);
127
+ if (req.user) {
128
+ req.log.debug(`User, '${req.user.username}', authenticated from session`);
129
+ }
130
+ else {
131
+ req.log.info('Failed to authenticate from session; ending session...');
132
+ delete req.session.user;
133
+ }
134
+ }
135
+ });
136
+ }
137
+ if (privacy) {
138
+ fastify.addHook('preHandler', async (req, reply) => {
139
+ if (!req.user && !whitelist.includes(req.url)) {
140
+ const r = await authenticate(req, reply);
141
+ if (!callback) {
142
+ const username = req.user?.username;
143
+ req.log.debug(`User, '${username}', authenticated`);
144
+ }
145
+ return r;
146
+ }
147
+ });
148
+ }
149
+ else {
150
+ fastify.get(pathPrefix + signInPath, authConfig, async (req, reply) => {
151
+ const r = await authenticate(req, reply);
152
+ if (!callback) {
153
+ req.log.debug(`User, '${req.user?.username}', authenticated`);
154
+ }
155
+ return (reply.sent
156
+ ? r
157
+ : redirect(req, reply));
158
+ });
159
+ }
160
+ if (callback) {
161
+ fastify.get(pathPrefix + callbackPath, authConfig, async (req, reply) => {
162
+ const r = await callback(req, reply);
163
+ req.log.debug(`User, '${req.user?.username}', authenticated`);
164
+ return (reply.sent
165
+ ? r
166
+ : redirect(req, reply));
167
+ });
168
+ }
169
+ fastify.get(pathPrefix + signOutPath, async (req, reply) => {
170
+ if (useSession && req.session?.user) {
171
+ delete req.user;
172
+ delete req.session.user;
173
+ }
174
+ const r = await terminate?.(req, reply);
175
+ req.log.debug('User logged out');
176
+ return (reply.sent
177
+ ? r
178
+ : redirect(req, reply));
179
+ });
180
+ }
181
+ };
182
+ exports.fastifyAuth = (0, fastify_plugin_1.default)(fastifyAuthPlugin, {
183
+ fastify: '5.x',
184
+ name: 'auth',
185
+ });
186
+ exports.default = exports.fastifyAuth;
187
+ var fastify_session_2 = require("@react-foundry/fastify-session");
188
+ Object.defineProperty(exports, "SessionStore", { enumerable: true, get: function () { return fastify_session_2.SessionStore; } });
package/dist/index.mjs ADDED
@@ -0,0 +1,148 @@
1
+ import fp from 'fastify-plugin';
2
+ import fastifyRateLimit from '@fastify/rate-limit';
3
+ import fastifySession, { SessionStore } from '@react-foundry/fastify-session';
4
+ import { basic } from './basic';
5
+ import { dummy } from './dummy';
6
+ import { headers } from './headers';
7
+ import { oidc } from './oidc';
8
+ export var AuthMethod;
9
+ (function (AuthMethod) {
10
+ AuthMethod["None"] = "none";
11
+ AuthMethod["Dummy"] = "dummy";
12
+ AuthMethod["Headers"] = "headers";
13
+ AuthMethod["Basic"] = "basic";
14
+ AuthMethod["OIDC"] = "oidc";
15
+ })(AuthMethod || (AuthMethod = {}));
16
+ ;
17
+ const isNone = (v) => v.method === AuthMethod.None || v.method === undefined;
18
+ const isDummy = (v) => v.method === AuthMethod.Dummy;
19
+ const isHeaders = (v) => v.method === AuthMethod.Headers;
20
+ const isBasic = (v) => v.method === AuthMethod.Basic;
21
+ const isOIDC = (v) => v.method === AuthMethod.OIDC;
22
+ const fastifyAuthPlugin = async (fastify, { privacy = true, pathPrefix = '/auth/', session = {}, signInPath = 'sign-in', signOutPath = 'sign-out', callbackPath = 'callback', redirectPath = '/', rateLimit: _rateLimit = {
23
+ max: 60,
24
+ timeWindow: 60000,
25
+ }, authRateLimit = {
26
+ max: 100,
27
+ timeWindow: 15 * 60000
28
+ }, ...methodOptions }) => {
29
+ const fullSessions = !!(session.store && session.store !== SessionStore.Cookie);
30
+ const serDes = (user, _req) => user;
31
+ const redact = (user, _req) => ({
32
+ username: user.username,
33
+ roles: user.roles
34
+ });
35
+ const _serialize = (fullSessions
36
+ ? serDes
37
+ : redact);
38
+ const redirect = async (_req, reply) => {
39
+ return reply.redirect(redirectPath, 302);
40
+ };
41
+ const rateLimit = _rateLimit && {
42
+ ..._rateLimit,
43
+ global: privacy
44
+ };
45
+ const authConfig = {
46
+ config: {
47
+ rateLimit: authRateLimit
48
+ }
49
+ };
50
+ const { authenticate, callback, deserialize = serDes, serialize = _serialize, terminate, wantSession } = await (isDummy(methodOptions) ? dummy(methodOptions, fullSessions)
51
+ : isHeaders(methodOptions) ? headers(methodOptions, fullSessions)
52
+ : isBasic(methodOptions) ? basic(methodOptions, fullSessions)
53
+ : isOIDC(methodOptions) ? oidc(methodOptions, fullSessions)
54
+ : {});
55
+ const useSession = wantSession || !privacy;
56
+ const whitelist = (callback
57
+ ? [pathPrefix + callbackPath, pathPrefix + signOutPath]
58
+ : [pathPrefix + signOutPath]);
59
+ fastify.decorateRequest('user', null);
60
+ if (!fastify.hasDecorator('rateLimit') && (authenticate || callback)) {
61
+ fastify.register(fastifyRateLimit, rateLimit);
62
+ }
63
+ if (authenticate) {
64
+ if (useSession) {
65
+ fastify.addHook('onSend', async (req, _reply, _payload) => {
66
+ if (req.user) {
67
+ if (req.session) {
68
+ req.session.user = await serialize(req.user, req);
69
+ }
70
+ else {
71
+ req.log.error('Unable to store session');
72
+ }
73
+ }
74
+ });
75
+ }
76
+ }
77
+ if (useSession || session.store) {
78
+ if (!session.store) {
79
+ fastify.log.info('Session required for authentication; registering plugin...');
80
+ }
81
+ fastify.register(fastifySession, session);
82
+ }
83
+ if (authenticate) {
84
+ if (useSession) {
85
+ fastify.addHook('preHandler', async (req, _reply) => {
86
+ if (req.session?.user) {
87
+ req.user = await deserialize(req.session.user, req);
88
+ if (req.user) {
89
+ req.log.debug(`User, '${req.user.username}', authenticated from session`);
90
+ }
91
+ else {
92
+ req.log.info('Failed to authenticate from session; ending session...');
93
+ delete req.session.user;
94
+ }
95
+ }
96
+ });
97
+ }
98
+ if (privacy) {
99
+ fastify.addHook('preHandler', async (req, reply) => {
100
+ if (!req.user && !whitelist.includes(req.url)) {
101
+ const r = await authenticate(req, reply);
102
+ if (!callback) {
103
+ const username = req.user?.username;
104
+ req.log.debug(`User, '${username}', authenticated`);
105
+ }
106
+ return r;
107
+ }
108
+ });
109
+ }
110
+ else {
111
+ fastify.get(pathPrefix + signInPath, authConfig, async (req, reply) => {
112
+ const r = await authenticate(req, reply);
113
+ if (!callback) {
114
+ req.log.debug(`User, '${req.user?.username}', authenticated`);
115
+ }
116
+ return (reply.sent
117
+ ? r
118
+ : redirect(req, reply));
119
+ });
120
+ }
121
+ if (callback) {
122
+ fastify.get(pathPrefix + callbackPath, authConfig, async (req, reply) => {
123
+ const r = await callback(req, reply);
124
+ req.log.debug(`User, '${req.user?.username}', authenticated`);
125
+ return (reply.sent
126
+ ? r
127
+ : redirect(req, reply));
128
+ });
129
+ }
130
+ fastify.get(pathPrefix + signOutPath, async (req, reply) => {
131
+ if (useSession && req.session?.user) {
132
+ delete req.user;
133
+ delete req.session.user;
134
+ }
135
+ const r = await terminate?.(req, reply);
136
+ req.log.debug('User logged out');
137
+ return (reply.sent
138
+ ? r
139
+ : redirect(req, reply));
140
+ });
141
+ }
142
+ };
143
+ export const fastifyAuth = fp(fastifyAuthPlugin, {
144
+ fastify: '5.x',
145
+ name: 'auth',
146
+ });
147
+ export default fastifyAuth;
148
+ export { SessionStore } from '@react-foundry/fastify-session';
package/dist/oidc.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { AuthBagger, UserProfile } from './common';
2
+ export type Options = {
3
+ issuer: string;
4
+ clientId: string;
5
+ clientSecret?: string;
6
+ redirectUri: string;
7
+ };
8
+ export type AuthInfo = UserProfile & {
9
+ accessToken?: string;
10
+ accessTokenValid?: boolean;
11
+ refreshToken?: string;
12
+ refreshTokenValid?: boolean;
13
+ idToken?: string;
14
+ idTokenValid?: boolean;
15
+ userinfo?: object;
16
+ };
17
+ export declare const oidc: AuthBagger<Options>;
18
+ export default oidc;
package/dist/oidc.js ADDED
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.oidc = void 0;
7
+ const base64url_1 = __importDefault(require("base64url"));
8
+ const openid_client_1 = require("openid-client");
9
+ const error_1 = require("@fastify/error");
10
+ const common_1 = require("./common");
11
+ const resourceToRoles = (acc, [x, y]) => ([
12
+ ...acc,
13
+ ...(y.roles?.map((e) => `${x}:${e}`) || [])
14
+ ]);
15
+ const BadSession = (0, error_1.createError)('FST_BAD_SESSION', 'Unable to verify session', 409);
16
+ const oidc = async ({ clientId, clientSecret, issuer, redirectUri: _redirectUri }, fullSessions) => {
17
+ openid_client_1.custom.setHttpOptionsDefaults({
18
+ timeout: 5000,
19
+ });
20
+ const redirectUri = _redirectUri + '/auth/callback';
21
+ const iss = await openid_client_1.Issuer.discover(issuer);
22
+ const client = new iss.Client({
23
+ client_id: clientId,
24
+ client_secret: clientSecret,
25
+ redirect_uris: [redirectUri],
26
+ token_endpoint_auth_method: (clientSecret
27
+ ? 'client_secret_basic'
28
+ : 'none')
29
+ });
30
+ const authInfo = (accessToken, refreshToken, idToken, userinfo = {}) => {
31
+ const extractJWTClaims = (token) => token && JSON.parse(base64url_1.default.decode(token.split('.')[1])) || {};
32
+ const accessClaims = extractJWTClaims(accessToken);
33
+ const idClaims = extractJWTClaims(idToken);
34
+ const refreshClaims = extractJWTClaims(refreshToken);
35
+ const data = {
36
+ ...accessClaims,
37
+ ...idClaims,
38
+ ...userinfo,
39
+ accessToken,
40
+ refreshToken,
41
+ idToken,
42
+ userinfo
43
+ };
44
+ const expiry = new Date((refreshClaims.exp || accessClaims.exp) * 1000);
45
+ const now = Math.floor(Date.now() / 1000);
46
+ const isValid = ({ nbf = 0, exp = 0 }) => ((nbf <= now) && (now < exp));
47
+ const accessTokenValid = isValid(accessClaims);
48
+ const refreshTokenValid = isValid(refreshClaims);
49
+ const idTokenValid = isValid(idClaims);
50
+ return {
51
+ provider: 'oidc',
52
+ id: data.sub,
53
+ displayName: data.displayName || data.name,
54
+ name: {
55
+ familyName: data.familyName || data.family_name,
56
+ givenName: data.givenName || data.given_name,
57
+ middleName: data.middleName || data.middle_name
58
+ },
59
+ emails: (data.email
60
+ ? [{ value: data.email }]
61
+ : undefined),
62
+ photos: (data.photo
63
+ ? [{ value: data.photo }]
64
+ : undefined),
65
+ username: data.username || data.preferred_username,
66
+ groups: data.groups,
67
+ roles: [
68
+ ...(data.roles || []),
69
+ ...(data.realm_access?.roles || []),
70
+ ...(Object.entries(data.resource_access || {}).reduce(resourceToRoles, []))
71
+ ].filter(common_1.id),
72
+ accessToken: data.accessToken,
73
+ accessTokenValid,
74
+ refreshToken: data.refreshToken,
75
+ refreshTokenValid,
76
+ idToken: data.idToken,
77
+ idTokenValid,
78
+ userinfo: data.userinfo,
79
+ expiry
80
+ };
81
+ };
82
+ const serialize = (user, req) => {
83
+ if (fullSessions) {
84
+ return user;
85
+ }
86
+ else {
87
+ const cookieLimit = 4096;
88
+ const encryptionCost = 1.5;
89
+ const smallEnough = (v) => (JSON.stringify(v).length * encryptionCost <= cookieLimit);
90
+ const payload = {
91
+ accessToken: user.accessToken,
92
+ refreshToken: user.refreshToken,
93
+ idToken: user.idToken,
94
+ userinfo: user.userinfo
95
+ };
96
+ if (smallEnough(payload)) {
97
+ return payload;
98
+ }
99
+ else {
100
+ delete payload.userinfo;
101
+ if (smallEnough(payload)) {
102
+ return payload;
103
+ }
104
+ else {
105
+ delete payload.idToken;
106
+ if (smallEnough(payload)) {
107
+ return payload;
108
+ }
109
+ else {
110
+ delete payload.refreshToken;
111
+ req.log.warn('Cannot fit refresh token in session; session will expire early');
112
+ return payload;
113
+ }
114
+ }
115
+ }
116
+ }
117
+ };
118
+ const deserialize = async ({ accessToken, refreshToken, idToken, userinfo }, req) => {
119
+ const user = authInfo(accessToken, refreshToken, idToken, userinfo || {});
120
+ if (user.username && user.accessTokenValid) {
121
+ return user;
122
+ }
123
+ else if (!user.accessTokenValid && refreshToken && user.refreshTokenValid) {
124
+ try {
125
+ const tokenSet = await client.refresh(refreshToken);
126
+ req.log.info('Obtained new access token');
127
+ const newUser = authInfo(tokenSet.access_token, tokenSet.refresh_token, tokenSet.id_token, userinfo || {});
128
+ if (newUser.username && newUser.accessTokenValid) {
129
+ return newUser;
130
+ }
131
+ else {
132
+ req.log.error('Access token was invalid');
133
+ return undefined;
134
+ }
135
+ }
136
+ catch (_err) {
137
+ req.log.error('Failed to obtain new access token');
138
+ return undefined;
139
+ }
140
+ }
141
+ else {
142
+ req.log.info('Access token has expired and cannot renew');
143
+ return undefined;
144
+ }
145
+ };
146
+ const authenticate = async (req, reply) => {
147
+ const codeVerifier = openid_client_1.generators.codeVerifier();
148
+ const codeChallenge = openid_client_1.generators.codeChallenge(codeVerifier);
149
+ const state = openid_client_1.generators.state();
150
+ const redirectTo = client.authorizationUrl({
151
+ scope: 'openid',
152
+ state,
153
+ code_challenge: codeChallenge,
154
+ code_challenge_method: 'S256'
155
+ });
156
+ const sessionObj = {
157
+ codeVerifier,
158
+ state
159
+ };
160
+ req.session.oidc = sessionObj;
161
+ return reply.redirect(redirectTo);
162
+ };
163
+ const callback = async (req, reply) => {
164
+ const sessionObj = (req.session.oidc || {});
165
+ delete req.session.oidc;
166
+ const { codeVerifier, state } = sessionObj;
167
+ if (!(codeVerifier && state)) {
168
+ throw new BadSession();
169
+ }
170
+ const checks = {
171
+ code_verifier: codeVerifier,
172
+ state
173
+ };
174
+ const params = client.callbackParams(req.raw);
175
+ const tokenSet = await client.callback(redirectUri, params, checks);
176
+ const userinfo = await client.userinfo(tokenSet);
177
+ const user = authInfo(tokenSet.access_token, tokenSet.refresh_token, tokenSet.id_token, userinfo);
178
+ if (user.username) {
179
+ req.user = user;
180
+ }
181
+ };
182
+ const terminate = async (req, reply) => {
183
+ const redirectTo = client.endSessionUrl({
184
+ post_logout_redirect_uri: _redirectUri
185
+ });
186
+ return reply.redirect(redirectTo);
187
+ };
188
+ return {
189
+ authenticate,
190
+ callback,
191
+ deserialize,
192
+ serialize,
193
+ terminate,
194
+ wantSession: true
195
+ };
196
+ };
197
+ exports.oidc = oidc;
198
+ exports.default = exports.oidc;
package/dist/oidc.mjs ADDED
@@ -0,0 +1,191 @@
1
+ import base64url from 'base64url';
2
+ import { Issuer, custom, generators } from 'openid-client';
3
+ import { createError } from '@fastify/error';
4
+ import { id } from './common';
5
+ const resourceToRoles = (acc, [x, y]) => ([
6
+ ...acc,
7
+ ...(y.roles?.map((e) => `${x}:${e}`) || [])
8
+ ]);
9
+ const BadSession = createError('FST_BAD_SESSION', 'Unable to verify session', 409);
10
+ export const oidc = async ({ clientId, clientSecret, issuer, redirectUri: _redirectUri }, fullSessions) => {
11
+ custom.setHttpOptionsDefaults({
12
+ timeout: 5000,
13
+ });
14
+ const redirectUri = _redirectUri + '/auth/callback';
15
+ const iss = await Issuer.discover(issuer);
16
+ const client = new iss.Client({
17
+ client_id: clientId,
18
+ client_secret: clientSecret,
19
+ redirect_uris: [redirectUri],
20
+ token_endpoint_auth_method: (clientSecret
21
+ ? 'client_secret_basic'
22
+ : 'none')
23
+ });
24
+ const authInfo = (accessToken, refreshToken, idToken, userinfo = {}) => {
25
+ const extractJWTClaims = (token) => token && JSON.parse(base64url.decode(token.split('.')[1])) || {};
26
+ const accessClaims = extractJWTClaims(accessToken);
27
+ const idClaims = extractJWTClaims(idToken);
28
+ const refreshClaims = extractJWTClaims(refreshToken);
29
+ const data = {
30
+ ...accessClaims,
31
+ ...idClaims,
32
+ ...userinfo,
33
+ accessToken,
34
+ refreshToken,
35
+ idToken,
36
+ userinfo
37
+ };
38
+ const expiry = new Date((refreshClaims.exp || accessClaims.exp) * 1000);
39
+ const now = Math.floor(Date.now() / 1000);
40
+ const isValid = ({ nbf = 0, exp = 0 }) => ((nbf <= now) && (now < exp));
41
+ const accessTokenValid = isValid(accessClaims);
42
+ const refreshTokenValid = isValid(refreshClaims);
43
+ const idTokenValid = isValid(idClaims);
44
+ return {
45
+ provider: 'oidc',
46
+ id: data.sub,
47
+ displayName: data.displayName || data.name,
48
+ name: {
49
+ familyName: data.familyName || data.family_name,
50
+ givenName: data.givenName || data.given_name,
51
+ middleName: data.middleName || data.middle_name
52
+ },
53
+ emails: (data.email
54
+ ? [{ value: data.email }]
55
+ : undefined),
56
+ photos: (data.photo
57
+ ? [{ value: data.photo }]
58
+ : undefined),
59
+ username: data.username || data.preferred_username,
60
+ groups: data.groups,
61
+ roles: [
62
+ ...(data.roles || []),
63
+ ...(data.realm_access?.roles || []),
64
+ ...(Object.entries(data.resource_access || {}).reduce(resourceToRoles, []))
65
+ ].filter(id),
66
+ accessToken: data.accessToken,
67
+ accessTokenValid,
68
+ refreshToken: data.refreshToken,
69
+ refreshTokenValid,
70
+ idToken: data.idToken,
71
+ idTokenValid,
72
+ userinfo: data.userinfo,
73
+ expiry
74
+ };
75
+ };
76
+ const serialize = (user, req) => {
77
+ if (fullSessions) {
78
+ return user;
79
+ }
80
+ else {
81
+ const cookieLimit = 4096;
82
+ const encryptionCost = 1.5;
83
+ const smallEnough = (v) => (JSON.stringify(v).length * encryptionCost <= cookieLimit);
84
+ const payload = {
85
+ accessToken: user.accessToken,
86
+ refreshToken: user.refreshToken,
87
+ idToken: user.idToken,
88
+ userinfo: user.userinfo
89
+ };
90
+ if (smallEnough(payload)) {
91
+ return payload;
92
+ }
93
+ else {
94
+ delete payload.userinfo;
95
+ if (smallEnough(payload)) {
96
+ return payload;
97
+ }
98
+ else {
99
+ delete payload.idToken;
100
+ if (smallEnough(payload)) {
101
+ return payload;
102
+ }
103
+ else {
104
+ delete payload.refreshToken;
105
+ req.log.warn('Cannot fit refresh token in session; session will expire early');
106
+ return payload;
107
+ }
108
+ }
109
+ }
110
+ }
111
+ };
112
+ const deserialize = async ({ accessToken, refreshToken, idToken, userinfo }, req) => {
113
+ const user = authInfo(accessToken, refreshToken, idToken, userinfo || {});
114
+ if (user.username && user.accessTokenValid) {
115
+ return user;
116
+ }
117
+ else if (!user.accessTokenValid && refreshToken && user.refreshTokenValid) {
118
+ try {
119
+ const tokenSet = await client.refresh(refreshToken);
120
+ req.log.info('Obtained new access token');
121
+ const newUser = authInfo(tokenSet.access_token, tokenSet.refresh_token, tokenSet.id_token, userinfo || {});
122
+ if (newUser.username && newUser.accessTokenValid) {
123
+ return newUser;
124
+ }
125
+ else {
126
+ req.log.error('Access token was invalid');
127
+ return undefined;
128
+ }
129
+ }
130
+ catch (_err) {
131
+ req.log.error('Failed to obtain new access token');
132
+ return undefined;
133
+ }
134
+ }
135
+ else {
136
+ req.log.info('Access token has expired and cannot renew');
137
+ return undefined;
138
+ }
139
+ };
140
+ const authenticate = async (req, reply) => {
141
+ const codeVerifier = generators.codeVerifier();
142
+ const codeChallenge = generators.codeChallenge(codeVerifier);
143
+ const state = generators.state();
144
+ const redirectTo = client.authorizationUrl({
145
+ scope: 'openid',
146
+ state,
147
+ code_challenge: codeChallenge,
148
+ code_challenge_method: 'S256'
149
+ });
150
+ const sessionObj = {
151
+ codeVerifier,
152
+ state
153
+ };
154
+ req.session.oidc = sessionObj;
155
+ return reply.redirect(redirectTo);
156
+ };
157
+ const callback = async (req, reply) => {
158
+ const sessionObj = (req.session.oidc || {});
159
+ delete req.session.oidc;
160
+ const { codeVerifier, state } = sessionObj;
161
+ if (!(codeVerifier && state)) {
162
+ throw new BadSession();
163
+ }
164
+ const checks = {
165
+ code_verifier: codeVerifier,
166
+ state
167
+ };
168
+ const params = client.callbackParams(req.raw);
169
+ const tokenSet = await client.callback(redirectUri, params, checks);
170
+ const userinfo = await client.userinfo(tokenSet);
171
+ const user = authInfo(tokenSet.access_token, tokenSet.refresh_token, tokenSet.id_token, userinfo);
172
+ if (user.username) {
173
+ req.user = user;
174
+ }
175
+ };
176
+ const terminate = async (req, reply) => {
177
+ const redirectTo = client.endSessionUrl({
178
+ post_logout_redirect_uri: _redirectUri
179
+ });
180
+ return reply.redirect(redirectTo);
181
+ };
182
+ return {
183
+ authenticate,
184
+ callback,
185
+ deserialize,
186
+ serialize,
187
+ terminate,
188
+ wantSession: true
189
+ };
190
+ };
191
+ export default oidc;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@react-foundry/fastify-auth",
3
+ "version": "0.1.0",
4
+ "description": "Authentication plugin for Fastify.",
5
+ "main": "dist/index.js",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.mjs",
10
+ "require": "./dist/index.js",
11
+ "default": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "/dist"
16
+ ],
17
+ "author": "Daniel A.C. Martin <npm@daniel-martin.co.uk> (http://daniel-martin.co.uk/)",
18
+ "license": "MIT",
19
+ "engines": {
20
+ "node": ">=22.0.0"
21
+ },
22
+ "dependencies": {
23
+ "@fastify/error": "^4.2.0",
24
+ "@fastify/rate-limit": "^10.3.0",
25
+ "base64url": "^3.0.1",
26
+ "fastify-plugin": "^5.1.0",
27
+ "openid-client": "^5.7.1",
28
+ "@react-foundry/fastify-session": "^0.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "fastify": "5.7.1",
32
+ "jest": "30.2.0",
33
+ "jest-environment-jsdom": "30.2.0",
34
+ "ts-jest": "29.4.6",
35
+ "typescript": "5.9.3"
36
+ },
37
+ "scripts": {
38
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
39
+ "build": "npm run build:esm && npm run build:cjs",
40
+ "build:esm": "tsc -m es2022 && find dist -name '*.js' -exec sh -c 'mv \"$0\" \"${0%.js}.mjs\"' {} \\;",
41
+ "build:cjs": "tsc",
42
+ "clean": "rm -rf dist tsconfig.tsbuildinfo"
43
+ },
44
+ "module": "dist/index.mjs",
45
+ "typings": "dist/index.d.ts"
46
+ }