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.
@@ -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,15 +192,15 @@ 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());
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=${(0, txstate_utils_1.htmlEncode)(redirectUrl)}">
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=${(0, txstate_utils_1.htmlEncode)(req.query.requestedUrl ?? (process.env.PUBLIC_URL || new URL('..', req.url).toString()))}">
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
- 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)
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 ?? new URL('.uaService', req.protocol + '://' + req.hostname).toString();
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.6.9",
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
- }