fastify-txstate 3.5.1 → 3.6.1
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/lib/index.d.ts +29 -0
- package/lib/index.js +1 -1
- package/lib/unified-auth.d.ts +18 -3
- package/lib/unified-auth.js +88 -33
- package/lib-esm/index.js +1 -0
- package/package.json +3 -3
package/lib/index.d.ts
CHANGED
|
@@ -25,6 +25,13 @@ export interface FastifyTxStateAuthInfo {
|
|
|
25
25
|
* If all else fails, you can sha256 the session token with a salt.
|
|
26
26
|
*/
|
|
27
27
|
sessionId: string;
|
|
28
|
+
/**
|
|
29
|
+
* The date that the session was created, if available. This is useful for considering
|
|
30
|
+
* tokens before a certain date as invalid. For instance, if you want a logout action
|
|
31
|
+
* to invalidate all tokens created until that point, you can compare this field against
|
|
32
|
+
* the last time they logged out.
|
|
33
|
+
*/
|
|
34
|
+
sessionCreatedAt?: Date;
|
|
28
35
|
/**
|
|
29
36
|
* Some authentication systems allow administrators to impersonate regular users, so that
|
|
30
37
|
* they can see what that user sees and troubleshoot issues. We still want to log the administrator
|
|
@@ -51,6 +58,24 @@ export interface FastifyTxStateAuthInfo {
|
|
|
51
58
|
* only a portion of the application's functionality would be available to them.
|
|
52
59
|
*/
|
|
53
60
|
scope?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The token or key that was used to authenticate the request. This is useful for
|
|
63
|
+
* making sub-requests to other APIs that can authenticate with the same token.
|
|
64
|
+
*/
|
|
65
|
+
token: string;
|
|
66
|
+
/**
|
|
67
|
+
* The issuer configuration for the token, if applicable. This helps you generate
|
|
68
|
+
* a proper logout url in multi-issuer environments.
|
|
69
|
+
*/
|
|
70
|
+
issuerConfig?: IssuerConfig;
|
|
71
|
+
}
|
|
72
|
+
export interface IssuerConfig {
|
|
73
|
+
iss: string;
|
|
74
|
+
url?: string;
|
|
75
|
+
publicKey?: string;
|
|
76
|
+
secret?: string;
|
|
77
|
+
validateUrl?: URL;
|
|
78
|
+
logoutUrl?: URL;
|
|
54
79
|
}
|
|
55
80
|
export interface FastifyTxStateOptions extends Partial<FastifyServerOptions> {
|
|
56
81
|
https?: http2.SecureServerOptions;
|
|
@@ -88,6 +113,10 @@ export interface FastifyTxStateOptions extends Partial<FastifyServerOptions> {
|
|
|
88
113
|
declare module 'fastify' {
|
|
89
114
|
interface FastifyRequest {
|
|
90
115
|
auth?: FastifyTxStateAuthInfo;
|
|
116
|
+
/**
|
|
117
|
+
* @deprecated Use `req.auth.token` instead. Just trying to keep everything contained.
|
|
118
|
+
* This will be removed in the next major version.
|
|
119
|
+
*/
|
|
91
120
|
token?: string;
|
|
92
121
|
}
|
|
93
122
|
interface FastifyReply {
|
package/lib/index.js
CHANGED
|
@@ -211,7 +211,7 @@ class Server {
|
|
|
211
211
|
DELETE: true
|
|
212
212
|
};
|
|
213
213
|
this.app.addHook('onRequest', async (req, res) => {
|
|
214
|
-
if (!authenticatedMethods[req.method] || req.routeOptions.url === '/health' || (this.swaggerEndpoint && req.routeOptions.url?.startsWith(this.swaggerEndpoint)))
|
|
214
|
+
if (!authenticatedMethods[req.method] || (0, txstate_utils_1.isBlank)(req.routeOptions.url) || req.routeOptions.url === '/health' || req.routeOptions.url === '/.uaService' || (this.swaggerEndpoint && req.routeOptions.url?.startsWith(this.swaggerEndpoint)))
|
|
215
215
|
return;
|
|
216
216
|
try {
|
|
217
217
|
req.auth = await config.authenticate(req);
|
package/lib/unified-auth.d.ts
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { type FastifyReply, type FastifyRequest } from 'fastify';
|
|
2
|
-
import { type FastifyInstanceTyped, type FastifyTxStateAuthInfo } from '.';
|
|
3
|
-
export
|
|
4
|
-
|
|
2
|
+
import { type IssuerConfig, type FastifyInstanceTyped, type FastifyTxStateAuthInfo } from '.';
|
|
3
|
+
export interface IssuerConfigRaw extends Omit<IssuerConfig, 'validateUrl' | 'logoutUrl'> {
|
|
4
|
+
validateUrl?: string;
|
|
5
|
+
logoutUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function unifiedAuthenticate(req: FastifyRequest, options?: {
|
|
8
|
+
authenticateAll?: boolean;
|
|
9
|
+
exceptRoutes?: Set<string>;
|
|
10
|
+
usingUaCookieRoutes?: boolean;
|
|
11
|
+
}): Promise<FastifyTxStateAuthInfo | undefined>;
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated Use unifiedAuthenticateWithOptions with { authenticateAll: true } instead.
|
|
14
|
+
*/
|
|
5
15
|
export declare function unifiedAuthenticateAll(req: FastifyRequest): Promise<FastifyTxStateAuthInfo>;
|
|
16
|
+
/**
|
|
17
|
+
* This function is available for server-side view code instead of a client-side application
|
|
18
|
+
* using a framework. It will automatically redirect the user to the Unified Auth login page
|
|
19
|
+
* and return true if they are not authenticated. Otherwise it simply returns false.
|
|
20
|
+
*/
|
|
6
21
|
export declare function requireCookieAuth(req: FastifyRequest, res: FastifyReply): Promise<boolean>;
|
|
7
22
|
export declare function registerUaCookieRoutes(app: FastifyInstanceTyped): void;
|
package/lib/unified-auth.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.uaCookieName = void 0;
|
|
4
3
|
exports.unifiedAuthenticate = unifiedAuthenticate;
|
|
5
4
|
exports.unifiedAuthenticateAll = unifiedAuthenticateAll;
|
|
6
5
|
exports.requireCookieAuth = requireCookieAuth;
|
|
@@ -12,7 +11,7 @@ let hasInit = false;
|
|
|
12
11
|
const issuerKeys = new Map();
|
|
13
12
|
const issuerConfig = new Map();
|
|
14
13
|
const trustedClients = new Set();
|
|
15
|
-
|
|
14
|
+
const uaCookieName = process.env.UA_COOKIE_NAME ?? (0, crypto_1.randomBytes)(16).toString('hex');
|
|
16
15
|
const tokenCache = new txstate_utils_1.Cache(async (token, req) => {
|
|
17
16
|
const claims = (0, jose_1.decodeJwt)(token);
|
|
18
17
|
let verifyKey;
|
|
@@ -68,10 +67,27 @@ function remoteJWKSet(jwkUrl) {
|
|
|
68
67
|
}
|
|
69
68
|
function processIssuerConfig(config) {
|
|
70
69
|
if (config.iss === 'unified-auth') {
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
const validateUrl = (0, txstate_utils_1.isNotBlank)(config.validateUrl)
|
|
71
|
+
? new URL(config.validateUrl, config.url)
|
|
72
|
+
: (0, txstate_utils_1.isNotBlank)(process.env.UA_URL)
|
|
73
|
+
? new URL(process.env.UA_URL + '/validateToken')
|
|
74
|
+
: new URL('validateToken', config.url);
|
|
75
|
+
const logoutUrl = (0, txstate_utils_1.isNotBlank)(config.logoutUrl)
|
|
76
|
+
? new URL(config.logoutUrl, config.url)
|
|
77
|
+
: (0, txstate_utils_1.isNotBlank)(process.env.UA_URL)
|
|
78
|
+
? new URL(process.env.UA_URL + '/logout')
|
|
79
|
+
: new URL('logout', config.url);
|
|
80
|
+
return {
|
|
81
|
+
...config,
|
|
82
|
+
validateUrl,
|
|
83
|
+
logoutUrl
|
|
84
|
+
};
|
|
73
85
|
}
|
|
74
|
-
return
|
|
86
|
+
return {
|
|
87
|
+
...config,
|
|
88
|
+
validateUrl: undefined,
|
|
89
|
+
logoutUrl: config.logoutUrl ? new URL(config.logoutUrl, config.url) : undefined
|
|
90
|
+
};
|
|
75
91
|
}
|
|
76
92
|
function init() {
|
|
77
93
|
hasInit = true;
|
|
@@ -97,11 +113,11 @@ function tokenFromReq(req) {
|
|
|
97
113
|
const m = req?.headers.authorization?.match(/^bearer (.*)$/i);
|
|
98
114
|
if (m != null)
|
|
99
115
|
return m[1];
|
|
100
|
-
const m2 = req?.headers.cookie?.match(new RegExp(`${
|
|
116
|
+
const m2 = req?.headers.cookie?.match(new RegExp(`${uaCookieName}=([^;]+)`));
|
|
101
117
|
if (m2 != null)
|
|
102
118
|
return m2[1];
|
|
103
119
|
}
|
|
104
|
-
async function
|
|
120
|
+
async function unifiedAuthenticateInternal(req) {
|
|
105
121
|
if (!hasInit)
|
|
106
122
|
init();
|
|
107
123
|
const token = tokenFromReq(req);
|
|
@@ -113,24 +129,48 @@ async function unifiedAuthenticate(req) {
|
|
|
113
129
|
await validateCache.get(token, payload);
|
|
114
130
|
req.token = token;
|
|
115
131
|
return {
|
|
132
|
+
token,
|
|
133
|
+
issuerConfig: payload.iss ? issuerConfig.get(payload.iss) : undefined,
|
|
116
134
|
username: payload.sub,
|
|
117
135
|
sessionId: payload.sub + '-' + payload.iat,
|
|
136
|
+
sessionCreatedAt: payload.iat ? new Date(payload.iat * 1000) : undefined,
|
|
118
137
|
clientId: payload.client_id,
|
|
119
138
|
impersonatedBy: payload.act?.sub,
|
|
120
139
|
scope: payload.scope
|
|
121
140
|
};
|
|
122
141
|
}
|
|
123
|
-
async function
|
|
124
|
-
const auth = await
|
|
125
|
-
if (
|
|
126
|
-
|
|
142
|
+
async function unifiedAuthenticate(req, options) {
|
|
143
|
+
const auth = await unifiedAuthenticateInternal(req);
|
|
144
|
+
if (options?.usingUaCookieRoutes) {
|
|
145
|
+
options.exceptRoutes ??= new Set();
|
|
146
|
+
options.exceptRoutes.add('/.uaService');
|
|
147
|
+
options.exceptRoutes.add('/.uaRedirect');
|
|
148
|
+
}
|
|
149
|
+
const isAuthenticatedRoute = options?.authenticateAll && (options.exceptRoutes == null || !options.exceptRoutes.has(req.routeOptions.url));
|
|
150
|
+
if (isAuthenticatedRoute) {
|
|
151
|
+
if ((0, txstate_utils_1.isBlank)(auth?.username))
|
|
152
|
+
throw new Error('Request requires authentication.');
|
|
153
|
+
}
|
|
127
154
|
return auth;
|
|
128
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* @deprecated Use unifiedAuthenticateWithOptions with { authenticateAll: true } instead.
|
|
158
|
+
*/
|
|
159
|
+
async function unifiedAuthenticateAll(req) {
|
|
160
|
+
return (await unifiedAuthenticate(req, { authenticateAll: true }));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* This function is available for server-side view code instead of a client-side application
|
|
164
|
+
* using a framework. It will automatically redirect the user to the Unified Auth login page
|
|
165
|
+
* and return true if they are not authenticated. Otherwise it simply returns false.
|
|
166
|
+
*/
|
|
129
167
|
async function requireCookieAuth(req, res) {
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
168
|
+
if ((0, txstate_utils_1.isBlank)(req.auth?.username)) {
|
|
169
|
+
const loginUrl = new URL(process.env.UA_URL + '/login');
|
|
170
|
+
loginUrl.searchParams.set('clientId', process.env.UA_CLIENTID);
|
|
171
|
+
loginUrl.searchParams.set('returnUrl', (0, txstate_utils_1.isNotBlank)(process.env.PUBLIC_URL) ? new URL(process.env.PUBLIC_URL + '/.uaService').toString() : new URL('/.uaService', req.url).toString());
|
|
172
|
+
loginUrl.searchParams.set('requestedUrl', req.originalUrl);
|
|
173
|
+
void res.redirect(loginUrl.toString());
|
|
134
174
|
return true;
|
|
135
175
|
}
|
|
136
176
|
else {
|
|
@@ -143,44 +183,59 @@ function registerUaCookieRoutes(app) {
|
|
|
143
183
|
headers: {
|
|
144
184
|
type: 'object',
|
|
145
185
|
properties: {
|
|
146
|
-
cookie: { type: 'string', pattern: `${
|
|
186
|
+
cookie: { type: 'string', pattern: `${uaCookieName}=[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+` }
|
|
147
187
|
},
|
|
148
188
|
required: ['cookie']
|
|
149
189
|
}
|
|
150
190
|
}
|
|
151
191
|
}, async (req, res) => {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
192
|
+
const redirectUrl = req.auth?.issuerConfig?.logoutUrl
|
|
193
|
+
? `${req.auth.issuerConfig.logoutUrl.toString()}?unifiedJwt=${encodeURIComponent(req.auth.token)}`
|
|
194
|
+
: (process.env.PUBLIC_URL || new URL('..', req.url).toString());
|
|
155
195
|
return res
|
|
156
|
-
.header('Set-Cookie', `${
|
|
157
|
-
.redirect(
|
|
196
|
+
.header('Set-Cookie', `${uaCookieName}=; Path=/; Secure; HttpOnly; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT`)
|
|
197
|
+
.redirect(redirectUrl);
|
|
158
198
|
});
|
|
159
199
|
app.get('/.uaService', {
|
|
160
200
|
schema: {
|
|
161
201
|
querystring: {
|
|
162
202
|
type: 'object',
|
|
163
203
|
properties: {
|
|
164
|
-
unifiedJwt: { type: 'string', pattern: '^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$' }
|
|
204
|
+
unifiedJwt: { type: 'string', pattern: '^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$' },
|
|
205
|
+
requestedUrl: { type: 'string', format: 'uri' }
|
|
165
206
|
},
|
|
166
207
|
required: ['unifiedJwt'],
|
|
167
208
|
additionalProperties: false
|
|
168
|
-
}
|
|
169
|
-
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}, async (req, res) => {
|
|
212
|
+
return res
|
|
213
|
+
.header('Set-Cookie', `${uaCookieName}=${req.query.unifiedJwt}; Path=/; Secure; HttpOnly; SameSite=Lax`)
|
|
214
|
+
.redirect(req.query.requestedUrl ?? (process.env.PUBLIC_URL || new URL('..', req.url).toString()));
|
|
215
|
+
});
|
|
216
|
+
/**
|
|
217
|
+
* In the case of a client-side application that uses the UA cookie to authenticate,
|
|
218
|
+
* the client code can detect a 401 from the API and redirect the user to this endpoint.
|
|
219
|
+
*
|
|
220
|
+
* This endpoint will redirect the browser to Unified Auth so that the client code does
|
|
221
|
+
* not need to have any configuration for Unified Auth.
|
|
222
|
+
*/
|
|
223
|
+
app.get('/.uaRedirect', {
|
|
224
|
+
schema: {
|
|
225
|
+
querystring: {
|
|
170
226
|
type: 'object',
|
|
171
227
|
properties: {
|
|
172
|
-
|
|
228
|
+
requestedUrl: { type: 'string', format: 'uri' }
|
|
173
229
|
},
|
|
174
|
-
|
|
230
|
+
additionalProperties: false
|
|
175
231
|
}
|
|
176
232
|
}
|
|
177
233
|
}, async (req, res) => {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
.redirect(decodeURIComponent(m[1]));
|
|
234
|
+
const loginUrl = new URL(process.env.UA_URL + '/login');
|
|
235
|
+
loginUrl.searchParams.set('clientId', process.env.UA_CLIENTID);
|
|
236
|
+
loginUrl.searchParams.set('returnUrl', new URL('.uaService', process.env.PUBLIC_URL || new URL(req.url, req.protocol + '://' + req.hostname)).toString());
|
|
237
|
+
if (req.query.requestedUrl)
|
|
238
|
+
loginUrl.searchParams.set('requestedUrl', req.query.requestedUrl);
|
|
239
|
+
return res.redirect(loginUrl.toString());
|
|
185
240
|
});
|
|
186
241
|
}
|
package/lib-esm/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export const ValidationError = ftxst.ValidationError
|
|
|
8
8
|
export const ValidationErrors = ftxst.ValidationErrors
|
|
9
9
|
export const unifiedAuthenticate = ftxst.unifiedAuthenticate
|
|
10
10
|
export const unifiedAuthenticateAll = ftxst.unifiedAuthenticateAll
|
|
11
|
+
export const registerUaCookieRoutes = ftxst.registerUaCookieRoutes
|
|
11
12
|
export const analyticsPlugin = ftxst.analyticsPlugin
|
|
12
13
|
export const AnalyticsClient = ftxst.AnalyticsClient
|
|
13
14
|
export const LoggingAnalyticsClient = ftxst.LoggingAnalyticsClient
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify-txstate",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"description": "A small wrapper for fastify providing a set of common conventions & utility functions we use.",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"@txstate-mws/fastify-shared": "^1.0.9",
|
|
26
26
|
"@types/ua-parser-js": ">=0.7.39",
|
|
27
27
|
"ajv-errors": "^3.0.0",
|
|
28
|
-
"ajv-formats": "^
|
|
28
|
+
"ajv-formats": "^3.0.0",
|
|
29
29
|
"fastify": "^4.9.2",
|
|
30
30
|
"fastify-plugin": "^4.5.1",
|
|
31
31
|
"http-status-codes": "^2.1.4",
|
|
32
|
-
"jose": "^
|
|
32
|
+
"jose": "^6.0.0",
|
|
33
33
|
"txstate-utils": "^1.9.5",
|
|
34
34
|
"ua-parser-js": "^1.0.37"
|
|
35
35
|
},
|