fastify-txstate 3.5.1 → 3.6.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/lib/index.d.ts +29 -0
- package/lib/index.js +1 -1
- package/lib/unified-auth.d.ts +18 -2
- package/lib/unified-auth.js +84 -28
- 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,23 @@
|
|
|
1
1
|
import { type FastifyReply, type FastifyRequest } from 'fastify';
|
|
2
|
-
import { type FastifyInstanceTyped, type FastifyTxStateAuthInfo } from '.';
|
|
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
|
+
}
|
|
3
7
|
export declare const uaCookieName: string;
|
|
4
|
-
export declare function unifiedAuthenticate(req: FastifyRequest
|
|
8
|
+
export declare function unifiedAuthenticate(req: FastifyRequest, options?: {
|
|
9
|
+
authenticateAll?: boolean;
|
|
10
|
+
exceptRoutes?: Set<string>;
|
|
11
|
+
usingUaCookieRoutes?: boolean;
|
|
12
|
+
}): Promise<FastifyTxStateAuthInfo | undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* @deprecated Use unifiedAuthenticateWithOptions with { authenticateAll: true } instead.
|
|
15
|
+
*/
|
|
5
16
|
export declare function unifiedAuthenticateAll(req: FastifyRequest): Promise<FastifyTxStateAuthInfo>;
|
|
17
|
+
/**
|
|
18
|
+
* This function is available for server-side view code instead of a client-side application
|
|
19
|
+
* using a framework. It will automatically redirect the user to the Unified Auth login page
|
|
20
|
+
* and return true if they are not authenticated. Otherwise it simply returns false.
|
|
21
|
+
*/
|
|
6
22
|
export declare function requireCookieAuth(req: FastifyRequest, res: FastifyReply): Promise<boolean>;
|
|
7
23
|
export declare function registerUaCookieRoutes(app: FastifyInstanceTyped): void;
|
package/lib/unified-auth.js
CHANGED
|
@@ -68,10 +68,27 @@ function remoteJWKSet(jwkUrl) {
|
|
|
68
68
|
}
|
|
69
69
|
function processIssuerConfig(config) {
|
|
70
70
|
if (config.iss === 'unified-auth') {
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const validateUrl = (0, txstate_utils_1.isNotBlank)(config.validateUrl)
|
|
72
|
+
? new URL(config.validateUrl, config.url)
|
|
73
|
+
: (0, txstate_utils_1.isNotBlank)(process.env.UA_URL)
|
|
74
|
+
? new URL(process.env.UA_URL + '/validateToken')
|
|
75
|
+
: new URL('validateToken', config.url);
|
|
76
|
+
const logoutUrl = (0, txstate_utils_1.isNotBlank)(config.logoutUrl)
|
|
77
|
+
? new URL(config.logoutUrl, config.url)
|
|
78
|
+
: (0, txstate_utils_1.isNotBlank)(process.env.UA_URL)
|
|
79
|
+
? new URL(process.env.UA_URL + '/logout')
|
|
80
|
+
: new URL('logout', config.url);
|
|
81
|
+
return {
|
|
82
|
+
...config,
|
|
83
|
+
validateUrl,
|
|
84
|
+
logoutUrl
|
|
85
|
+
};
|
|
73
86
|
}
|
|
74
|
-
return
|
|
87
|
+
return {
|
|
88
|
+
...config,
|
|
89
|
+
validateUrl: undefined,
|
|
90
|
+
logoutUrl: config.logoutUrl ? new URL(config.logoutUrl, config.url) : undefined
|
|
91
|
+
};
|
|
75
92
|
}
|
|
76
93
|
function init() {
|
|
77
94
|
hasInit = true;
|
|
@@ -101,7 +118,7 @@ function tokenFromReq(req) {
|
|
|
101
118
|
if (m2 != null)
|
|
102
119
|
return m2[1];
|
|
103
120
|
}
|
|
104
|
-
async function
|
|
121
|
+
async function unifiedAuthenticateInternal(req) {
|
|
105
122
|
if (!hasInit)
|
|
106
123
|
init();
|
|
107
124
|
const token = tokenFromReq(req);
|
|
@@ -113,24 +130,48 @@ async function unifiedAuthenticate(req) {
|
|
|
113
130
|
await validateCache.get(token, payload);
|
|
114
131
|
req.token = token;
|
|
115
132
|
return {
|
|
133
|
+
token,
|
|
134
|
+
issuerConfig: payload.iss ? issuerConfig.get(payload.iss) : undefined,
|
|
116
135
|
username: payload.sub,
|
|
117
136
|
sessionId: payload.sub + '-' + payload.iat,
|
|
137
|
+
sessionCreatedAt: payload.iat ? new Date(payload.iat * 1000) : undefined,
|
|
118
138
|
clientId: payload.client_id,
|
|
119
139
|
impersonatedBy: payload.act?.sub,
|
|
120
140
|
scope: payload.scope
|
|
121
141
|
};
|
|
122
142
|
}
|
|
123
|
-
async function
|
|
124
|
-
const auth = await
|
|
125
|
-
if (
|
|
126
|
-
|
|
143
|
+
async function unifiedAuthenticate(req, options) {
|
|
144
|
+
const auth = await unifiedAuthenticateInternal(req);
|
|
145
|
+
if (options?.usingUaCookieRoutes) {
|
|
146
|
+
options.exceptRoutes ??= new Set();
|
|
147
|
+
options.exceptRoutes.add('/.uaService');
|
|
148
|
+
options.exceptRoutes.add('/.uaRedirect');
|
|
149
|
+
}
|
|
150
|
+
const isAuthenticatedRoute = options?.authenticateAll && (options.exceptRoutes == null || !options.exceptRoutes.has(req.routeOptions.url));
|
|
151
|
+
if (isAuthenticatedRoute) {
|
|
152
|
+
if ((0, txstate_utils_1.isBlank)(auth?.username))
|
|
153
|
+
throw new Error('Request requires authentication.');
|
|
154
|
+
}
|
|
127
155
|
return auth;
|
|
128
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* @deprecated Use unifiedAuthenticateWithOptions with { authenticateAll: true } instead.
|
|
159
|
+
*/
|
|
160
|
+
async function unifiedAuthenticateAll(req) {
|
|
161
|
+
return (await unifiedAuthenticate(req, { authenticateAll: true }));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* This function is available for server-side view code instead of a client-side application
|
|
165
|
+
* using a framework. It will automatically redirect the user to the Unified Auth login page
|
|
166
|
+
* and return true if they are not authenticated. Otherwise it simply returns false.
|
|
167
|
+
*/
|
|
129
168
|
async function requireCookieAuth(req, res) {
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
169
|
+
if ((0, txstate_utils_1.isBlank)(req.auth?.username)) {
|
|
170
|
+
const loginUrl = new URL(process.env.UA_URL + '/login');
|
|
171
|
+
loginUrl.searchParams.set('clientId', process.env.UA_CLIENTID);
|
|
172
|
+
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());
|
|
173
|
+
loginUrl.searchParams.set('requestedUrl', req.originalUrl);
|
|
174
|
+
void res.redirect(loginUrl.toString());
|
|
134
175
|
return true;
|
|
135
176
|
}
|
|
136
177
|
else {
|
|
@@ -149,38 +190,53 @@ function registerUaCookieRoutes(app) {
|
|
|
149
190
|
}
|
|
150
191
|
}
|
|
151
192
|
}, async (req, res) => {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
193
|
+
const redirectUrl = req.auth?.issuerConfig?.logoutUrl
|
|
194
|
+
? `${req.auth.issuerConfig.logoutUrl.toString()}?unifiedJwt=${encodeURIComponent(req.auth.token)}`
|
|
195
|
+
: (process.env.PUBLIC_URL || new URL('..', req.url).toString());
|
|
155
196
|
return res
|
|
156
197
|
.header('Set-Cookie', `${exports.uaCookieName}=; Path=/; Secure; HttpOnly; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT`)
|
|
157
|
-
.redirect(
|
|
198
|
+
.redirect(redirectUrl);
|
|
158
199
|
});
|
|
159
200
|
app.get('/.uaService', {
|
|
160
201
|
schema: {
|
|
161
202
|
querystring: {
|
|
162
203
|
type: 'object',
|
|
163
204
|
properties: {
|
|
164
|
-
unifiedJwt: { type: 'string', pattern: '^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$' }
|
|
205
|
+
unifiedJwt: { type: 'string', pattern: '^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$' },
|
|
206
|
+
requestedUrl: { type: 'string', format: 'uri' }
|
|
165
207
|
},
|
|
166
208
|
required: ['unifiedJwt'],
|
|
167
209
|
additionalProperties: false
|
|
168
|
-
}
|
|
169
|
-
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}, async (req, res) => {
|
|
213
|
+
return res
|
|
214
|
+
.header('Set-Cookie', `${exports.uaCookieName}=${req.query.unifiedJwt}; Path=/; Secure; HttpOnly; SameSite=Lax`)
|
|
215
|
+
.redirect(req.query.requestedUrl ?? (process.env.PUBLIC_URL || new URL('..', req.url).toString()));
|
|
216
|
+
});
|
|
217
|
+
/**
|
|
218
|
+
* In the case of a client-side application that uses the UA cookie to authenticate,
|
|
219
|
+
* the client code can detect a 401 from the API and redirect the user to this endpoint.
|
|
220
|
+
*
|
|
221
|
+
* This endpoint will redirect the browser to Unified Auth so that the client code does
|
|
222
|
+
* not need to have any configuration for Unified Auth.
|
|
223
|
+
*/
|
|
224
|
+
app.get('/.uaRedirect', {
|
|
225
|
+
schema: {
|
|
226
|
+
querystring: {
|
|
170
227
|
type: 'object',
|
|
171
228
|
properties: {
|
|
172
|
-
|
|
229
|
+
requestedUrl: { type: 'string', format: 'uri' }
|
|
173
230
|
},
|
|
174
|
-
|
|
231
|
+
additionalProperties: false
|
|
175
232
|
}
|
|
176
233
|
}
|
|
177
234
|
}, async (req, res) => {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
.redirect(decodeURIComponent(m[1]));
|
|
235
|
+
const loginUrl = new URL(process.env.UA_URL + '/login');
|
|
236
|
+
loginUrl.searchParams.set('clientId', process.env.UA_CLIENTID);
|
|
237
|
+
loginUrl.searchParams.set('returnUrl', new URL('.uaService', process.env.PUBLIC_URL || new URL(req.url, req.protocol + '://' + req.hostname)).toString());
|
|
238
|
+
if (req.query.requestedUrl)
|
|
239
|
+
loginUrl.searchParams.set('requestedUrl', req.query.requestedUrl);
|
|
240
|
+
return res.redirect(loginUrl.toString());
|
|
185
241
|
});
|
|
186
242
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify-txstate",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
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
|
},
|