fastify-txstate 3.6.9 → 4.0.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.
@@ -1,178 +1,34 @@
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");
10
- let hasInit = false;
11
- const issuerKeys = new Map();
12
- const issuerConfig = new Map();
13
- 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);
19
- let verifyKey;
20
- if (claims.iss && issuerKeys.has(claims.iss))
21
- verifyKey = issuerKeys.get(claims.iss);
22
- if (!verifyKey) {
23
- req.log.warn(`Received token with issuer: ${claims.iss} but JWT secret could not be found. The server may be misconfigured or the user may have presented a JWT from an untrusted issuer.`);
24
- return undefined;
25
- }
26
- try {
27
- const { payload } = await (0, jose_1.jwtVerify)(token, verifyKey);
28
- if (trustedClients.size && !trustedClients.has(payload.client_id)) {
29
- req.log.warn(`Received token with untrusted client_id: ${payload.client_id}.`);
30
- return undefined;
31
- }
32
- return payload;
33
- }
34
- 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')
37
- req.log.error(e);
38
- return undefined;
39
- }
40
- }, { freshseconds: 3600 });
41
- const validateCache = new txstate_utils_1.Cache(async (token, payload) => {
42
- const config = issuerConfig.get(payload.iss);
43
- if (!config?.validateUrl)
44
- return;
45
- // avoid checking for deauth until the token is more than 5 minutes old
46
- if (new Date(payload.iat * 1000) > new Date(new Date().getTime() - 1000 * 60 * 5))
47
- return;
48
- const validateUrl = new URL(config.validateUrl);
49
- validateUrl.searchParams.set('unifiedJwt', token);
50
- const resp = await fetch(validateUrl);
51
- const validate = await resp.json();
52
- if (!validate.valid)
53
- 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
- });
55
- const jwkCache = new txstate_utils_1.Cache(async (url) => {
56
- const { keys } = await (await fetch(url)).json();
57
- const publicKeyByKid = {};
58
- for (const jwk of keys) {
59
- if (jwk.kid)
60
- publicKeyByKid[jwk.kid] = await (0, jose_1.importJWK)(jwk);
61
- }
62
- return publicKeyByKid;
63
- });
64
- function remoteJWKSet(jwkUrl) {
65
- return async (protectedHeader) => {
66
- const publicKeyByKid = await jwkCache.get(jwkUrl);
67
- return publicKeyByKid[protectedHeader.kid];
68
- };
69
- }
70
- function processIssuerConfig(config) {
71
- if (config.iss === 'unified-auth') {
72
- const validateUrl = (0, txstate_utils_1.isNotBlank)(config.validateUrl)
73
- ? new URL(config.validateUrl, config.url)
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
- };
85
- }
86
- return {
87
- ...config,
88
- validateUrl: undefined,
89
- logoutUrl: config.logoutUrl ? new URL(config.logoutUrl, config.url) : undefined
90
- };
91
- }
92
- function init() {
93
- hasInit = true;
94
- if (process.env.JWT_TRUSTED_ISSUERS) {
95
- const issuers = (0, txstate_utils_1.toArray)(JSON.parse(process.env.JWT_TRUSTED_ISSUERS));
96
- for (const issuer of issuers) {
97
- issuerConfig.set(issuer.iss, processIssuerConfig(issuer));
98
- if (issuer.iss === 'unified-auth')
99
- issuerKeys.set(issuer.iss, remoteJWKSet(issuer.url));
100
- else if (issuer.url)
101
- issuerKeys.set(issuer.iss, (0, jose_1.createRemoteJWKSet)(new URL(issuer.url)));
102
- else if (issuer.publicKey)
103
- issuerKeys.set(issuer.iss, (0, crypto_1.createPublicKey)(issuer.publicKey));
104
- else if (issuer.secret)
105
- issuerKeys.set(issuer.iss, (0, crypto_1.createSecretKey)(Buffer.from(issuer.secret, 'ascii')));
106
- }
107
- }
108
- for (const clientId of (process.env.JWT_TRUSTED_CLIENTIDS?.split(',').filter(txstate_utils_1.isNotBlank).map(clientId => clientId.trim()) ?? [])) {
109
- trustedClients.add(clientId);
110
- }
1
+ import { htmlEncode, isBlank, isNotBlank } from 'txstate-utils';
2
+ import { getIssuerConfig, jwtAuthenticate, registeredExceptRoutes, registeredOptionalRoutes, uaCookieName } from "./jwt-auth.js";
3
+ import { apiBaseUrl, uiBaseUrl } from "./server.js";
4
+ function uaServiceUrl(req) {
5
+ return apiBaseUrl(req) + '/.uaService';
111
6
  }
112
- function tokenFromReq(req) {
113
- const m = req?.headers.authorization?.match(/^bearer (.*)$/i);
114
- if (m != null)
115
- return m[1];
116
- const m2 = req?.headers.cookie?.match(uaCookieNameRegex);
117
- if (m2 != null)
118
- return m2[1];
119
- }
120
- async function unifiedAuthenticateInternal(req) {
121
- if (!hasInit)
122
- init();
123
- const token = tokenFromReq(req);
124
- if (!token)
125
- return undefined;
126
- const payload = await tokenCache.get(token, req);
127
- if (!payload)
128
- return undefined;
129
- await validateCache.get(token, payload);
130
- req.token = token;
131
- return {
132
- token,
133
- issuerConfig: payload.iss ? issuerConfig.get(payload.iss) : undefined,
134
- username: payload.sub,
135
- sessionId: payload.sub + '-' + payload.iat,
136
- sessionCreatedAt: payload.iat ? new Date(payload.iat * 1000) : undefined,
137
- clientId: payload.client_id,
138
- impersonatedBy: payload.act?.sub,
139
- scope: payload.scope
140
- };
141
- }
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
- options.optionalRoutes ??= new Set();
149
- options.optionalRoutes.add('/.uaLogout');
150
- }
151
- 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)) {
156
- throw new Error('Request requires authentication.');
157
- }
158
- return isNoAuthenticationRoute ? undefined : auth;
7
+ /**
8
+ * @deprecated Use `jwtAuthenticate(options)` instead. Note the new shape: `jwtAuthenticate`
9
+ * is now a factory that takes options up front and returns the authenticator function
10
+ * (`authenticate: jwtAuthenticate({ authenticateAll: true })`), so the options actually
11
+ * take effect when wired into `new Server({ authenticate })`.
12
+ */
13
+ export async function unifiedAuthenticate(req, options) {
14
+ return await jwtAuthenticate(options)(req);
159
15
  }
160
16
  /**
161
- * @deprecated Use unifiedAuthenticateWithOptions with { authenticateAll: true } instead.
17
+ * @deprecated Use `jwtAuthenticate({ authenticateAll: true })` instead.
162
18
  */
163
- async function unifiedAuthenticateAll(req) {
164
- return (await unifiedAuthenticate(req, { authenticateAll: true }));
19
+ export async function unifiedAuthenticateAll(req) {
20
+ return (await jwtAuthenticate({ authenticateAll: true })(req));
165
21
  }
166
22
  /**
167
23
  * This function is available for server-side view code instead of a client-side application
168
24
  * using a framework. It will automatically redirect the user to the Unified Auth login page
169
25
  * and return true if they are not authenticated. Otherwise it simply returns false.
170
26
  */
171
- async function requireCookieAuth(req, res) {
172
- if ((0, txstate_utils_1.isBlank)(req.auth?.username)) {
27
+ export async function requireCookieAuthUa(req, res) {
28
+ if (isBlank(req.auth?.username)) {
173
29
  const loginUrl = new URL(process.env.UA_URL + '/login');
174
- loginUrl.searchParams.set('clientId', process.env.UA_CLIENTID);
175
- loginUrl.searchParams.set('returnUrl', uaServiceUrl ?? new URL('/.uaService', req.url).toString());
30
+ loginUrl.searchParams.set('clientId', (process.env.UA_COOKIE_CLIENTID ?? process.env.UA_CLIENTID));
31
+ loginUrl.searchParams.set('returnUrl', uaServiceUrl(req));
176
32
  loginUrl.searchParams.set('requestedUrl', req.originalUrl);
177
33
  void res.redirect(loginUrl.toString());
178
34
  return true;
@@ -181,7 +37,16 @@ async function requireCookieAuth(req, res) {
181
37
  return false;
182
38
  }
183
39
  }
184
- function registerUaCookieRoutes(app) {
40
+ /**
41
+ * @deprecated Use requireCookieAuthUa instead.
42
+ */
43
+ export async function requireCookieAuth(req, res) {
44
+ return await requireCookieAuthUa(req, res);
45
+ }
46
+ export function registerUaCookieRoutes(app) {
47
+ registeredExceptRoutes.add('/.uaService');
48
+ registeredExceptRoutes.add('/.uaRedirect');
49
+ registeredOptionalRoutes.add('/.uaLogout');
185
50
  app.get('/.uaLogout', {
186
51
  schema: {
187
52
  headers: {
@@ -193,15 +58,15 @@ function registerUaCookieRoutes(app) {
193
58
  }
194
59
  }
195
60
  }, async (req, res) => {
196
- const redirectUrl = req.auth?.issuerConfig?.logoutUrl && (0, txstate_utils_1.isNotBlank)(req.auth.token)
61
+ const redirectUrl = req.auth?.issuerConfig?.logoutUrl && isNotBlank(req.auth.token)
197
62
  ? `${req.auth.issuerConfig.logoutUrl.toString()}?unifiedJwt=${encodeURIComponent(req.auth.token)}`
198
- : (process.env.PUBLIC_URL || new URL('..', req.url).toString());
63
+ : uiBaseUrl(req);
199
64
  void res.header('Set-Cookie', `${uaCookieName}=; Path=/; Secure; HttpOnly; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT`);
200
65
  return `<!DOCTYPE html>
201
66
  <html lang="en">
202
67
  <head>
203
68
  <meta charset="UTF-8">
204
- <meta http-equiv="refresh" content="0; url=${(0, txstate_utils_1.htmlEncode)(redirectUrl)}">
69
+ <meta http-equiv="refresh" content="0; url=${htmlEncode(redirectUrl)}">
205
70
  <title>Logging out...</title>
206
71
  </head>
207
72
  <body>
@@ -221,13 +86,18 @@ function registerUaCookieRoutes(app) {
221
86
  }
222
87
  }
223
88
  }, async (req, res) => {
89
+ const destination = req.query.requestedUrl ?? uiBaseUrl(req);
90
+ if (req.query.requestedUrl && req.originChecker && !req.originChecker.check(req.query.requestedUrl, req.hostname)) {
91
+ void res.status(403);
92
+ return 'Requested URL failed origin check.';
93
+ }
224
94
  void res.header('Set-Cookie', `${uaCookieName}=${req.query.unifiedJwt}; Path=/; Secure; HttpOnly; SameSite=Lax`);
225
95
  void res.type('text/html');
226
96
  return `<!DOCTYPE html>
227
97
  <html lang="en">
228
98
  <head>
229
99
  <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()))}">
100
+ <meta http-equiv="refresh" content="0; url=${htmlEncode(destination)}">
231
101
  <title>Logging in...</title>
232
102
  </head>
233
103
  <body>
@@ -252,14 +122,18 @@ function registerUaCookieRoutes(app) {
252
122
  }
253
123
  }
254
124
  }, async (req, res) => {
255
- const loginUrl = (0, txstate_utils_1.isNotBlank)(process.env.UA_URL)
125
+ if (req.query.requestedUrl && req.originChecker && !req.originChecker.check(req.query.requestedUrl, req.hostname)) {
126
+ void res.status(403);
127
+ return 'Requested URL failed origin check.';
128
+ }
129
+ const loginUrl = isNotBlank(process.env.UA_URL)
256
130
  ? new URL(process.env.UA_URL + '/login')
257
- : new URL('login', issuerConfig.get('unified-auth')?.url);
258
- 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();
131
+ : new URL('login', getIssuerConfig('unified-auth')?.url);
132
+ loginUrl.searchParams.set('clientId', process.env.UA_COOKIE_CLIENTID ?? process.env.UA_CLIENTID ?? process.env.JWT_TRUSTED_CLIENTIDS.split(',')[0]);
133
+ const returnUrl = uaServiceUrl(req);
260
134
  loginUrl.searchParams.set('returnUrl', returnUrl);
261
135
  if (req.query.requestedUrl)
262
136
  loginUrl.searchParams.set('requestedUrl', req.query.requestedUrl);
263
- return res.redirect(loginUrl.toString());
137
+ return await res.redirect(loginUrl.toString());
264
138
  });
265
139
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "fastify-txstate",
3
- "version": "3.6.9",
3
+ "version": "4.0.1",
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": "^154.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
- }