@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 +22 -0
- package/README.md +117 -0
- package/dist/basic.d.ts +10 -0
- package/dist/basic.js +40 -0
- package/dist/basic.mjs +36 -0
- package/dist/common.d.ts +48 -0
- package/dist/common.js +9 -0
- package/dist/common.mjs +4 -0
- package/dist/dummy.d.ts +8 -0
- package/dist/dummy.js +14 -0
- package/dist/dummy.mjs +10 -0
- package/dist/headers.d.ts +8 -0
- package/dist/headers.js +24 -0
- package/dist/headers.mjs +20 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +188 -0
- package/dist/index.mjs +148 -0
- package/dist/oidc.d.ts +18 -0
- package/dist/oidc.js +198 -0
- package/dist/oidc.mjs +191 -0
- package/package.json +46 -0
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/
|
package/dist/basic.d.ts
ADDED
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;
|
package/dist/common.d.ts
ADDED
|
@@ -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;
|
package/dist/common.mjs
ADDED
package/dist/dummy.d.ts
ADDED
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
package/dist/headers.js
ADDED
|
@@ -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;
|
package/dist/headers.mjs
ADDED
|
@@ -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;
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|