fastify-txstate 3.6.9 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +354 -7
- package/lib/analytics.d.ts +5 -2
- package/lib/analytics.js +28 -33
- package/lib/error.d.ts +1 -10
- package/lib/error.js +7 -28
- package/lib/filestorage.d.ts +1 -1
- package/lib/filestorage.js +27 -31
- package/lib/index.d.ts +8 -194
- package/lib/index.js +8 -422
- package/lib/oauth.d.ts +71 -0
- package/lib/oauth.js +507 -0
- package/lib/postformdata.js +13 -17
- package/{lib-esm/index.d.ts → lib/server.d.ts} +69 -37
- package/lib/server.js +440 -0
- package/lib/unified-auth.d.ts +2 -2
- package/lib/unified-auth.js +55 -47
- package/package.json +27 -25
- package/lib-esm/index.js +0 -20
- package/lib-esm/package.json +0 -3
package/lib/unified-auth.js
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
exports.requireCookieAuth = requireCookieAuth;
|
|
6
|
-
exports.registerUaCookieRoutes = registerUaCookieRoutes;
|
|
7
|
-
const crypto_1 = require("crypto");
|
|
8
|
-
const jose_1 = require("jose");
|
|
9
|
-
const txstate_utils_1 = require("txstate-utils");
|
|
1
|
+
import { createPublicKey, createSecretKey, randomBytes } from 'node:crypto';
|
|
2
|
+
import { createRemoteJWKSet, decodeJwt, jwtVerify, importJWK } from 'jose';
|
|
3
|
+
import { Cache, htmlEncode, isBlank, isNotBlank, toArray } from 'txstate-utils';
|
|
4
|
+
import { apiBaseUrl, uiBaseUrl } from "./server.js";
|
|
10
5
|
let hasInit = false;
|
|
11
6
|
const issuerKeys = new Map();
|
|
12
7
|
const issuerConfig = new Map();
|
|
13
8
|
const trustedClients = new Set();
|
|
14
|
-
const uaCookieName = process.env.UA_COOKIE_NAME ??
|
|
15
|
-
const uaCookieNameRegex = new RegExp(`${uaCookieName}=([^;]+)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
const uaCookieName = process.env.UA_COOKIE_NAME ?? randomBytes(16).toString('hex');
|
|
10
|
+
const uaCookieNameRegex = new RegExp(`${uaCookieName}=([^;]+)`, 'v');
|
|
11
|
+
function uaServiceUrl(req) {
|
|
12
|
+
return apiBaseUrl(req) + '/.uaService';
|
|
13
|
+
}
|
|
14
|
+
const tokenCache = new Cache(async (token, req) => {
|
|
15
|
+
const claims = decodeJwt(token);
|
|
19
16
|
let verifyKey;
|
|
20
17
|
if (claims.iss && issuerKeys.has(claims.iss))
|
|
21
18
|
verifyKey = issuerKeys.get(claims.iss);
|
|
@@ -24,7 +21,7 @@ const tokenCache = new txstate_utils_1.Cache(async (token, req) => {
|
|
|
24
21
|
return undefined;
|
|
25
22
|
}
|
|
26
23
|
try {
|
|
27
|
-
const { payload } = await
|
|
24
|
+
const { payload } = await jwtVerify(token, verifyKey);
|
|
28
25
|
if (trustedClients.size && !trustedClients.has(payload.client_id)) {
|
|
29
26
|
req.log.warn(`Received token with untrusted client_id: ${payload.client_id}.`);
|
|
30
27
|
return undefined;
|
|
@@ -32,13 +29,14 @@ const tokenCache = new txstate_utils_1.Cache(async (token, req) => {
|
|
|
32
29
|
return payload;
|
|
33
30
|
}
|
|
34
31
|
catch (e) {
|
|
35
|
-
// squelch errors
|
|
36
|
-
|
|
32
|
+
// squelch expected token errors — bad signatures and expirations show as 401 in the access log
|
|
33
|
+
const code = e.code;
|
|
34
|
+
if (code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' && code !== 'ERR_JWT_EXPIRED')
|
|
37
35
|
req.log.error(e);
|
|
38
36
|
return undefined;
|
|
39
37
|
}
|
|
40
38
|
}, { freshseconds: 3600 });
|
|
41
|
-
const validateCache = new
|
|
39
|
+
const validateCache = new Cache(async (token, payload) => {
|
|
42
40
|
const config = issuerConfig.get(payload.iss);
|
|
43
41
|
if (!config?.validateUrl)
|
|
44
42
|
return;
|
|
@@ -52,12 +50,12 @@ const validateCache = new txstate_utils_1.Cache(async (token, payload) => {
|
|
|
52
50
|
if (!validate.valid)
|
|
53
51
|
throw new Error(validate.reason ?? 'Your session has been ended on another device or in another browser tab/window. It\'s also possible your NetID is no longer active.');
|
|
54
52
|
});
|
|
55
|
-
const jwkCache = new
|
|
53
|
+
const jwkCache = new Cache(async (url) => {
|
|
56
54
|
const { keys } = await (await fetch(url)).json();
|
|
57
55
|
const publicKeyByKid = {};
|
|
58
56
|
for (const jwk of keys) {
|
|
59
57
|
if (jwk.kid)
|
|
60
|
-
publicKeyByKid[jwk.kid] = await
|
|
58
|
+
publicKeyByKid[jwk.kid] = await importJWK(jwk);
|
|
61
59
|
}
|
|
62
60
|
return publicKeyByKid;
|
|
63
61
|
});
|
|
@@ -69,12 +67,12 @@ function remoteJWKSet(jwkUrl) {
|
|
|
69
67
|
}
|
|
70
68
|
function processIssuerConfig(config) {
|
|
71
69
|
if (config.iss === 'unified-auth') {
|
|
72
|
-
const validateUrl =
|
|
70
|
+
const validateUrl = isNotBlank(config.validateUrl)
|
|
73
71
|
? new URL(config.validateUrl, config.url)
|
|
74
72
|
: new URL('validateToken', config.url);
|
|
75
|
-
const logoutUrl =
|
|
73
|
+
const logoutUrl = isNotBlank(config.logoutUrl)
|
|
76
74
|
? new URL(config.logoutUrl, config.url)
|
|
77
|
-
:
|
|
75
|
+
: isNotBlank(process.env.UA_URL)
|
|
78
76
|
? new URL(process.env.UA_URL + '/logout')
|
|
79
77
|
: new URL('logout', config.url);
|
|
80
78
|
return {
|
|
@@ -92,25 +90,25 @@ function processIssuerConfig(config) {
|
|
|
92
90
|
function init() {
|
|
93
91
|
hasInit = true;
|
|
94
92
|
if (process.env.JWT_TRUSTED_ISSUERS) {
|
|
95
|
-
const issuers =
|
|
93
|
+
const issuers = toArray(JSON.parse(process.env.JWT_TRUSTED_ISSUERS));
|
|
96
94
|
for (const issuer of issuers) {
|
|
97
95
|
issuerConfig.set(issuer.iss, processIssuerConfig(issuer));
|
|
98
96
|
if (issuer.iss === 'unified-auth')
|
|
99
97
|
issuerKeys.set(issuer.iss, remoteJWKSet(issuer.url));
|
|
100
98
|
else if (issuer.url)
|
|
101
|
-
issuerKeys.set(issuer.iss,
|
|
99
|
+
issuerKeys.set(issuer.iss, createRemoteJWKSet(new URL(issuer.url)));
|
|
102
100
|
else if (issuer.publicKey)
|
|
103
|
-
issuerKeys.set(issuer.iss,
|
|
101
|
+
issuerKeys.set(issuer.iss, createPublicKey(issuer.publicKey));
|
|
104
102
|
else if (issuer.secret)
|
|
105
|
-
issuerKeys.set(issuer.iss,
|
|
103
|
+
issuerKeys.set(issuer.iss, createSecretKey(Buffer.from(issuer.secret, 'ascii')));
|
|
106
104
|
}
|
|
107
105
|
}
|
|
108
|
-
for (const clientId of (process.env.JWT_TRUSTED_CLIENTIDS?.split(',').filter(
|
|
106
|
+
for (const clientId of (process.env.JWT_TRUSTED_CLIENTIDS?.split(',').filter(isNotBlank).map(clientId => clientId.trim()) ?? [])) {
|
|
109
107
|
trustedClients.add(clientId);
|
|
110
108
|
}
|
|
111
109
|
}
|
|
112
110
|
function tokenFromReq(req) {
|
|
113
|
-
const m = req?.headers.authorization?.match(/^bearer (.*)$/
|
|
111
|
+
const m = req?.headers.authorization?.match(/^bearer (.*)$/iv);
|
|
114
112
|
if (m != null)
|
|
115
113
|
return m[1];
|
|
116
114
|
const m2 = req?.headers.cookie?.match(uaCookieNameRegex);
|
|
@@ -126,8 +124,9 @@ async function unifiedAuthenticateInternal(req) {
|
|
|
126
124
|
const payload = await tokenCache.get(token, req);
|
|
127
125
|
if (!payload)
|
|
128
126
|
return undefined;
|
|
127
|
+
if (payload.exp && payload.exp * 1000 <= Date.now())
|
|
128
|
+
return undefined;
|
|
129
129
|
await validateCache.get(token, payload);
|
|
130
|
-
req.token = token;
|
|
131
130
|
return {
|
|
132
131
|
token,
|
|
133
132
|
issuerConfig: payload.iss ? issuerConfig.get(payload.iss) : undefined,
|
|
@@ -139,7 +138,7 @@ async function unifiedAuthenticateInternal(req) {
|
|
|
139
138
|
scope: payload.scope
|
|
140
139
|
};
|
|
141
140
|
}
|
|
142
|
-
async function unifiedAuthenticate(req, options) {
|
|
141
|
+
export async function unifiedAuthenticate(req, options) {
|
|
143
142
|
const auth = await unifiedAuthenticateInternal(req);
|
|
144
143
|
if (options?.usingUaCookieRoutes) {
|
|
145
144
|
options.exceptRoutes ??= new Set();
|
|
@@ -149,10 +148,10 @@ async function unifiedAuthenticate(req, options) {
|
|
|
149
148
|
options.optionalRoutes.add('/.uaLogout');
|
|
150
149
|
}
|
|
151
150
|
const isNoAuthenticationRoute = options?.exceptRoutes?.has(req.routeOptions.url);
|
|
152
|
-
const requiresAuthenticationRoute = options?.authenticateAll
|
|
153
|
-
!options
|
|
154
|
-
!options
|
|
155
|
-
if (requiresAuthenticationRoute &&
|
|
151
|
+
const requiresAuthenticationRoute = options?.authenticateAll
|
|
152
|
+
&& !options.exceptRoutes?.has(req.routeOptions.url)
|
|
153
|
+
&& !options.optionalRoutes?.has(req.routeOptions.url);
|
|
154
|
+
if (requiresAuthenticationRoute && isBlank(auth?.username)) {
|
|
156
155
|
throw new Error('Request requires authentication.');
|
|
157
156
|
}
|
|
158
157
|
return isNoAuthenticationRoute ? undefined : auth;
|
|
@@ -160,7 +159,7 @@ async function unifiedAuthenticate(req, options) {
|
|
|
160
159
|
/**
|
|
161
160
|
* @deprecated Use unifiedAuthenticateWithOptions with { authenticateAll: true } instead.
|
|
162
161
|
*/
|
|
163
|
-
async function unifiedAuthenticateAll(req) {
|
|
162
|
+
export async function unifiedAuthenticateAll(req) {
|
|
164
163
|
return (await unifiedAuthenticate(req, { authenticateAll: true }));
|
|
165
164
|
}
|
|
166
165
|
/**
|
|
@@ -168,11 +167,11 @@ async function unifiedAuthenticateAll(req) {
|
|
|
168
167
|
* using a framework. It will automatically redirect the user to the Unified Auth login page
|
|
169
168
|
* and return true if they are not authenticated. Otherwise it simply returns false.
|
|
170
169
|
*/
|
|
171
|
-
async function requireCookieAuth(req, res) {
|
|
172
|
-
if (
|
|
170
|
+
export async function requireCookieAuth(req, res) {
|
|
171
|
+
if (isBlank(req.auth?.username)) {
|
|
173
172
|
const loginUrl = new URL(process.env.UA_URL + '/login');
|
|
174
173
|
loginUrl.searchParams.set('clientId', process.env.UA_CLIENTID);
|
|
175
|
-
loginUrl.searchParams.set('returnUrl', uaServiceUrl
|
|
174
|
+
loginUrl.searchParams.set('returnUrl', uaServiceUrl(req));
|
|
176
175
|
loginUrl.searchParams.set('requestedUrl', req.originalUrl);
|
|
177
176
|
void res.redirect(loginUrl.toString());
|
|
178
177
|
return true;
|
|
@@ -181,7 +180,7 @@ async function requireCookieAuth(req, res) {
|
|
|
181
180
|
return false;
|
|
182
181
|
}
|
|
183
182
|
}
|
|
184
|
-
function registerUaCookieRoutes(app) {
|
|
183
|
+
export function registerUaCookieRoutes(app) {
|
|
185
184
|
app.get('/.uaLogout', {
|
|
186
185
|
schema: {
|
|
187
186
|
headers: {
|
|
@@ -193,15 +192,15 @@ function registerUaCookieRoutes(app) {
|
|
|
193
192
|
}
|
|
194
193
|
}
|
|
195
194
|
}, async (req, res) => {
|
|
196
|
-
const redirectUrl = req.auth?.issuerConfig?.logoutUrl &&
|
|
195
|
+
const redirectUrl = req.auth?.issuerConfig?.logoutUrl && isNotBlank(req.auth.token)
|
|
197
196
|
? `${req.auth.issuerConfig.logoutUrl.toString()}?unifiedJwt=${encodeURIComponent(req.auth.token)}`
|
|
198
|
-
: (
|
|
197
|
+
: uiBaseUrl(req);
|
|
199
198
|
void res.header('Set-Cookie', `${uaCookieName}=; Path=/; Secure; HttpOnly; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT`);
|
|
200
199
|
return `<!DOCTYPE html>
|
|
201
200
|
<html lang="en">
|
|
202
201
|
<head>
|
|
203
202
|
<meta charset="UTF-8">
|
|
204
|
-
<meta http-equiv="refresh" content="0; url=${
|
|
203
|
+
<meta http-equiv="refresh" content="0; url=${htmlEncode(redirectUrl)}">
|
|
205
204
|
<title>Logging out...</title>
|
|
206
205
|
</head>
|
|
207
206
|
<body>
|
|
@@ -221,13 +220,18 @@ function registerUaCookieRoutes(app) {
|
|
|
221
220
|
}
|
|
222
221
|
}
|
|
223
222
|
}, async (req, res) => {
|
|
223
|
+
const destination = req.query.requestedUrl ?? uiBaseUrl(req);
|
|
224
|
+
if (req.query.requestedUrl && req.originChecker && !req.originChecker.check(req.query.requestedUrl, req.hostname)) {
|
|
225
|
+
void res.status(403);
|
|
226
|
+
return 'Requested URL failed origin check.';
|
|
227
|
+
}
|
|
224
228
|
void res.header('Set-Cookie', `${uaCookieName}=${req.query.unifiedJwt}; Path=/; Secure; HttpOnly; SameSite=Lax`);
|
|
225
229
|
void res.type('text/html');
|
|
226
230
|
return `<!DOCTYPE html>
|
|
227
231
|
<html lang="en">
|
|
228
232
|
<head>
|
|
229
233
|
<meta charset="UTF-8">
|
|
230
|
-
<meta http-equiv="refresh" content="0; url=${
|
|
234
|
+
<meta http-equiv="refresh" content="0; url=${htmlEncode(destination)}">
|
|
231
235
|
<title>Logging in...</title>
|
|
232
236
|
</head>
|
|
233
237
|
<body>
|
|
@@ -252,14 +256,18 @@ function registerUaCookieRoutes(app) {
|
|
|
252
256
|
}
|
|
253
257
|
}
|
|
254
258
|
}, async (req, res) => {
|
|
255
|
-
|
|
259
|
+
if (req.query.requestedUrl && req.originChecker && !req.originChecker.check(req.query.requestedUrl, req.hostname)) {
|
|
260
|
+
void res.status(403);
|
|
261
|
+
return 'Requested URL failed origin check.';
|
|
262
|
+
}
|
|
263
|
+
const loginUrl = isNotBlank(process.env.UA_URL)
|
|
256
264
|
? new URL(process.env.UA_URL + '/login')
|
|
257
265
|
: new URL('login', issuerConfig.get('unified-auth')?.url);
|
|
258
266
|
loginUrl.searchParams.set('clientId', process.env.UA_CLIENTID ?? process.env.JWT_TRUSTED_CLIENTIDS.split(',')[0]);
|
|
259
|
-
const returnUrl = uaServiceUrl
|
|
267
|
+
const returnUrl = uaServiceUrl(req);
|
|
260
268
|
loginUrl.searchParams.set('returnUrl', returnUrl);
|
|
261
269
|
if (req.query.requestedUrl)
|
|
262
270
|
loginUrl.searchParams.set('requestedUrl', req.query.requestedUrl);
|
|
263
|
-
return res.redirect(loginUrl.toString());
|
|
271
|
+
return await res.redirect(loginUrl.toString());
|
|
264
272
|
});
|
|
265
273
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify-txstate",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "A small wrapper for fastify providing a set of common conventions & utility functions we use.",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"exports": {
|
|
6
7
|
".": {
|
|
7
8
|
"types": "./lib/index.d.ts",
|
|
8
|
-
"
|
|
9
|
-
"import": "./lib-esm/index.js"
|
|
9
|
+
"import": "./lib/index.js"
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
"types": "./lib/index.d.ts",
|
|
@@ -14,37 +14,37 @@
|
|
|
14
14
|
"prepublishOnly": "npm run build",
|
|
15
15
|
"build": "rm -rf lib && tsc",
|
|
16
16
|
"test": "./test.sh",
|
|
17
|
-
"
|
|
18
|
-
"testserver": "node -
|
|
17
|
+
"nodetest": "node --experimental-transform-types --no-warnings --test test/**/*.ts",
|
|
18
|
+
"testserver": "node --experimental-transform-types --no-warnings testserver/index.ts",
|
|
19
|
+
"lint": "eslint src test testserver"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
|
-
"@elastic/elasticsearch": "^
|
|
22
|
-
"@fastify/swagger": "^
|
|
23
|
-
"@fastify/swagger-ui": "^
|
|
24
|
-
"@fastify/type-provider-json-schema-to-ts": "^
|
|
22
|
+
"@elastic/elasticsearch": "^9.0.0",
|
|
23
|
+
"@fastify/swagger": "^9.0.0",
|
|
24
|
+
"@fastify/swagger-ui": "^5.0.0",
|
|
25
|
+
"@fastify/type-provider-json-schema-to-ts": "^5.0.0",
|
|
25
26
|
"@txstate-mws/fastify-shared": "^1.0.9",
|
|
26
|
-
"
|
|
27
|
+
"ajv": "^8.20.0",
|
|
27
28
|
"ajv-errors": "^3.0.0",
|
|
28
29
|
"ajv-formats": "^3.0.0",
|
|
29
|
-
"fastify": "^
|
|
30
|
-
"fastify-plugin": "^4.5.1",
|
|
30
|
+
"fastify": "^5.0.0",
|
|
31
31
|
"http-status-codes": "^2.1.4",
|
|
32
|
-
"jose": "^
|
|
32
|
+
"jose": "^6.0.0",
|
|
33
|
+
"openapi-types": "^12.1.3",
|
|
34
|
+
"pino": "^10.3.1",
|
|
33
35
|
"txstate-utils": "^1.9.5",
|
|
34
|
-
"ua-parser-js": "^
|
|
36
|
+
"ua-parser-js": "^2.0.0"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
|
-
"@fastify/multipart": "^
|
|
38
|
-
"@
|
|
39
|
-
"@types/
|
|
39
|
+
"@fastify/multipart": "^10.0.0",
|
|
40
|
+
"@stylistic/eslint-plugin": "^5.0.0",
|
|
41
|
+
"@types/chai": "^5.2.3",
|
|
40
42
|
"@types/node": "^24.0.0",
|
|
41
|
-
"axios": "^1.
|
|
42
|
-
"chai": "^
|
|
43
|
-
"eslint-config-
|
|
43
|
+
"axios": "^1.15.0",
|
|
44
|
+
"chai": "^6.0.0",
|
|
45
|
+
"eslint-config-love": "^153.0.0",
|
|
44
46
|
"json-schema-to-ts": "^3.0.1",
|
|
45
|
-
"
|
|
46
|
-
"ts-node": "^10.2.1",
|
|
47
|
-
"typescript": "^5.0.4"
|
|
47
|
+
"typescript": "^6.0.0"
|
|
48
48
|
},
|
|
49
49
|
"repository": {
|
|
50
50
|
"type": "git",
|
|
@@ -56,8 +56,10 @@
|
|
|
56
56
|
"url": "https://github.com/txstate-etc/fastify-txstate/issues"
|
|
57
57
|
},
|
|
58
58
|
"homepage": "https://github.com/txstate-etc/fastify-txstate#readme",
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=20"
|
|
61
|
+
},
|
|
59
62
|
"files": [
|
|
60
|
-
"lib/**/*"
|
|
61
|
-
"lib-esm/**/*"
|
|
63
|
+
"lib/**/*"
|
|
62
64
|
]
|
|
63
65
|
}
|
package/lib-esm/index.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import ftxst from '../lib/index.js'
|
|
2
|
-
|
|
3
|
-
export const devLogger = ftxst.devLogger
|
|
4
|
-
export const prodLogger = ftxst.prodLogger
|
|
5
|
-
export const HttpError = ftxst.HttpError
|
|
6
|
-
export const FailedValidationError = ftxst.FailedValidationError
|
|
7
|
-
export const ValidationError = ftxst.ValidationError
|
|
8
|
-
export const ValidationErrors = ftxst.ValidationErrors
|
|
9
|
-
export const unifiedAuthenticate = ftxst.unifiedAuthenticate
|
|
10
|
-
export const unifiedAuthenticateAll = ftxst.unifiedAuthenticateAll
|
|
11
|
-
export const registerUaCookieRoutes = ftxst.registerUaCookieRoutes
|
|
12
|
-
export const analyticsPlugin = ftxst.analyticsPlugin
|
|
13
|
-
export const AnalyticsClient = ftxst.AnalyticsClient
|
|
14
|
-
export const LoggingAnalyticsClient = ftxst.LoggingAnalyticsClient
|
|
15
|
-
export const ElasticAnalyticsClient = ftxst.ElasticAnalyticsClient
|
|
16
|
-
export const postFormData = ftxst.postFormData
|
|
17
|
-
export const readableToWebReadable = ftxst.readableToWebReadable
|
|
18
|
-
export const FileSystemHandler = ftxst.FileSystemHandler
|
|
19
|
-
export const fileHandler = ftxst.fileHandler
|
|
20
|
-
export default ftxst.default
|
package/lib-esm/package.json
DELETED