fastify-txstate 3.6.8 → 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.
@@ -1,21 +1,18 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.unifiedAuthenticate = unifiedAuthenticate;
4
- exports.unifiedAuthenticateAll = unifiedAuthenticateAll;
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 ?? (0, crypto_1.randomBytes)(16).toString('hex');
15
- const uaCookieNameRegex = new RegExp(`${uaCookieName}=([^;]+)`);
16
- const uaServiceUrl = (0, txstate_utils_1.isNotBlank)(process.env.PUBLIC_URL) ? process.env.PUBLIC_URL + (process.env.PUBLIC_URL.endsWith('/') ? '' : '/') + '.uaService' : undefined;
17
- const tokenCache = new txstate_utils_1.Cache(async (token, req) => {
18
- const claims = (0, jose_1.decodeJwt)(token);
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 (0, jose_1.jwtVerify)(token, verifyKey);
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 about bad tokens, we can already see the 401 in the log
36
- if (e.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED')
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 txstate_utils_1.Cache(async (token, payload) => {
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 txstate_utils_1.Cache(async (url) => {
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 (0, jose_1.importJWK)(jwk);
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 = (0, txstate_utils_1.isNotBlank)(config.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 = (0, txstate_utils_1.isNotBlank)(config.logoutUrl)
73
+ const logoutUrl = isNotBlank(config.logoutUrl)
76
74
  ? new URL(config.logoutUrl, config.url)
77
- : (0, txstate_utils_1.isNotBlank)(process.env.UA_URL)
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 = (0, txstate_utils_1.toArray)(JSON.parse(process.env.JWT_TRUSTED_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, (0, jose_1.createRemoteJWKSet)(new URL(issuer.url)));
99
+ issuerKeys.set(issuer.iss, createRemoteJWKSet(new URL(issuer.url)));
102
100
  else if (issuer.publicKey)
103
- issuerKeys.set(issuer.iss, (0, crypto_1.createPublicKey)(issuer.publicKey));
101
+ issuerKeys.set(issuer.iss, createPublicKey(issuer.publicKey));
104
102
  else if (issuer.secret)
105
- issuerKeys.set(issuer.iss, (0, crypto_1.createSecretKey)(Buffer.from(issuer.secret, 'ascii')));
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(txstate_utils_1.isNotBlank).map(clientId => clientId.trim()) ?? [])) {
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 (.*)$/i);
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?.exceptRoutes?.has(req.routeOptions.url) &&
154
- !options?.optionalRoutes?.has(req.routeOptions.url);
155
- if (requiresAuthenticationRoute && (0, txstate_utils_1.isBlank)(auth?.username)) {
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 ((0, txstate_utils_1.isBlank)(req.auth?.username)) {
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 ?? new URL('/.uaService', req.url).toString());
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,12 +192,20 @@ function registerUaCookieRoutes(app) {
193
192
  }
194
193
  }
195
194
  }, async (req, res) => {
196
- const redirectUrl = req.auth?.issuerConfig?.logoutUrl && (0, txstate_utils_1.isNotBlank)(req.auth.token)
195
+ const redirectUrl = req.auth?.issuerConfig?.logoutUrl && isNotBlank(req.auth.token)
197
196
  ? `${req.auth.issuerConfig.logoutUrl.toString()}?unifiedJwt=${encodeURIComponent(req.auth.token)}`
198
- : (process.env.PUBLIC_URL || new URL('..', req.url).toString());
199
- return res
200
- .header('Set-Cookie', `${uaCookieName}=; Path=/; Secure; HttpOnly; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT`)
201
- .redirect(redirectUrl);
197
+ : uiBaseUrl(req);
198
+ void res.header('Set-Cookie', `${uaCookieName}=; Path=/; Secure; HttpOnly; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT`);
199
+ return `<!DOCTYPE html>
200
+ <html lang="en">
201
+ <head>
202
+ <meta charset="UTF-8">
203
+ <meta http-equiv="refresh" content="0; url=${htmlEncode(redirectUrl)}">
204
+ <title>Logging out...</title>
205
+ </head>
206
+ <body>
207
+ </body>
208
+ </html>`;
202
209
  });
203
210
  app.get('/.uaService', {
204
211
  schema: {
@@ -213,9 +220,23 @@ function registerUaCookieRoutes(app) {
213
220
  }
214
221
  }
215
222
  }, async (req, res) => {
216
- return res
217
- .header('Set-Cookie', `${uaCookieName}=${req.query.unifiedJwt}; Path=/; Secure; HttpOnly; SameSite=Lax`)
218
- .redirect(req.query.requestedUrl ?? (process.env.PUBLIC_URL || new URL('..', req.url).toString()));
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
+ }
228
+ void res.header('Set-Cookie', `${uaCookieName}=${req.query.unifiedJwt}; Path=/; Secure; HttpOnly; SameSite=Lax`);
229
+ void res.type('text/html');
230
+ return `<!DOCTYPE html>
231
+ <html lang="en">
232
+ <head>
233
+ <meta charset="UTF-8">
234
+ <meta http-equiv="refresh" content="0; url=${htmlEncode(destination)}">
235
+ <title>Logging in...</title>
236
+ </head>
237
+ <body>
238
+ </body>
239
+ </html>`;
219
240
  });
220
241
  /**
221
242
  * In the case of a client-side application that uses the UA cookie to authenticate,
@@ -235,14 +256,18 @@ function registerUaCookieRoutes(app) {
235
256
  }
236
257
  }
237
258
  }, async (req, res) => {
238
- const loginUrl = (0, txstate_utils_1.isNotBlank)(process.env.UA_URL)
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)
239
264
  ? new URL(process.env.UA_URL + '/login')
240
265
  : new URL('login', issuerConfig.get('unified-auth')?.url);
241
266
  loginUrl.searchParams.set('clientId', process.env.UA_CLIENTID ?? process.env.JWT_TRUSTED_CLIENTIDS.split(',')[0]);
242
- const returnUrl = uaServiceUrl ?? new URL('.uaService', req.protocol + '://' + req.hostname).toString();
267
+ const returnUrl = uaServiceUrl(req);
243
268
  loginUrl.searchParams.set('returnUrl', returnUrl);
244
269
  if (req.query.requestedUrl)
245
270
  loginUrl.searchParams.set('requestedUrl', req.query.requestedUrl);
246
- return res.redirect(loginUrl.toString());
271
+ return await res.redirect(loginUrl.toString());
247
272
  });
248
273
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "fastify-txstate",
3
- "version": "3.6.8",
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
- "require": "./lib/index.js",
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
- "mocha": "TS_NODE_PROJECT=test/tsconfig.json mocha -r ts-node/register test/**/*.ts",
18
- "testserver": "node -r ts-node/register --no-warnings testserver/index.ts"
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": "^8.13.0",
22
- "@fastify/swagger": "^8.14.0",
23
- "@fastify/swagger-ui": "^3.0.0",
24
- "@fastify/type-provider-json-schema-to-ts": "^3.0.0",
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
- "@types/ua-parser-js": ">=0.7.39",
27
+ "ajv": "^8.20.0",
27
28
  "ajv-errors": "^3.0.0",
28
29
  "ajv-formats": "^3.0.0",
29
- "fastify": "^4.9.2",
30
- "fastify-plugin": "^4.5.1",
30
+ "fastify": "^5.0.0",
31
31
  "http-status-codes": "^2.1.4",
32
- "jose": "^5.0.0 || ^6.0.0",
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": "^1.0.37"
36
+ "ua-parser-js": "^2.0.0"
35
37
  },
36
38
  "devDependencies": {
37
- "@fastify/multipart": "^8.0.0",
38
- "@types/chai": "^4.2.14",
39
- "@types/mocha": "^10.0.0",
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.6.8",
42
- "chai": "^4.2.0",
43
- "eslint-config-standard-with-typescript": "^43.0.0",
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
- "mocha": "^10.0.0",
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
@@ -1,3 +0,0 @@
1
- {
2
- "type": "module"
3
- }