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