fastify-txstate 3.5.1 → 3.6.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/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,23 @@
1
1
  import { type FastifyReply, type FastifyRequest } from 'fastify';
2
- import { type FastifyInstanceTyped, type FastifyTxStateAuthInfo } from '.';
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
+ }
3
7
  export declare const uaCookieName: string;
4
- export declare function unifiedAuthenticate(req: FastifyRequest): Promise<FastifyTxStateAuthInfo | undefined>;
8
+ export declare function unifiedAuthenticate(req: FastifyRequest, options?: {
9
+ authenticateAll?: boolean;
10
+ exceptRoutes?: Set<string>;
11
+ usingUaCookieRoutes?: boolean;
12
+ }): Promise<FastifyTxStateAuthInfo | undefined>;
13
+ /**
14
+ * @deprecated Use unifiedAuthenticateWithOptions with { authenticateAll: true } instead.
15
+ */
5
16
  export declare function unifiedAuthenticateAll(req: FastifyRequest): Promise<FastifyTxStateAuthInfo>;
17
+ /**
18
+ * This function is available for server-side view code instead of a client-side application
19
+ * using a framework. It will automatically redirect the user to the Unified Auth login page
20
+ * and return true if they are not authenticated. Otherwise it simply returns false.
21
+ */
6
22
  export declare function requireCookieAuth(req: FastifyRequest, res: FastifyReply): Promise<boolean>;
7
23
  export declare function registerUaCookieRoutes(app: FastifyInstanceTyped): void;
@@ -68,10 +68,27 @@ function remoteJWKSet(jwkUrl) {
68
68
  }
69
69
  function processIssuerConfig(config) {
70
70
  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('/');
71
+ const validateUrl = (0, txstate_utils_1.isNotBlank)(config.validateUrl)
72
+ ? new URL(config.validateUrl, config.url)
73
+ : (0, txstate_utils_1.isNotBlank)(process.env.UA_URL)
74
+ ? new URL(process.env.UA_URL + '/validateToken')
75
+ : new URL('validateToken', config.url);
76
+ const logoutUrl = (0, txstate_utils_1.isNotBlank)(config.logoutUrl)
77
+ ? new URL(config.logoutUrl, config.url)
78
+ : (0, txstate_utils_1.isNotBlank)(process.env.UA_URL)
79
+ ? new URL(process.env.UA_URL + '/logout')
80
+ : new URL('logout', config.url);
81
+ return {
82
+ ...config,
83
+ validateUrl,
84
+ logoutUrl
85
+ };
73
86
  }
74
- return config;
87
+ return {
88
+ ...config,
89
+ validateUrl: undefined,
90
+ logoutUrl: config.logoutUrl ? new URL(config.logoutUrl, config.url) : undefined
91
+ };
75
92
  }
76
93
  function init() {
77
94
  hasInit = true;
@@ -101,7 +118,7 @@ function tokenFromReq(req) {
101
118
  if (m2 != null)
102
119
  return m2[1];
103
120
  }
104
- async function unifiedAuthenticate(req) {
121
+ async function unifiedAuthenticateInternal(req) {
105
122
  if (!hasInit)
106
123
  init();
107
124
  const token = tokenFromReq(req);
@@ -113,24 +130,48 @@ async function unifiedAuthenticate(req) {
113
130
  await validateCache.get(token, payload);
114
131
  req.token = token;
115
132
  return {
133
+ token,
134
+ issuerConfig: payload.iss ? issuerConfig.get(payload.iss) : undefined,
116
135
  username: payload.sub,
117
136
  sessionId: payload.sub + '-' + payload.iat,
137
+ sessionCreatedAt: payload.iat ? new Date(payload.iat * 1000) : undefined,
118
138
  clientId: payload.client_id,
119
139
  impersonatedBy: payload.act?.sub,
120
140
  scope: payload.scope
121
141
  };
122
142
  }
123
- async function unifiedAuthenticateAll(req) {
124
- const auth = await unifiedAuthenticate(req);
125
- if (!auth?.username.length)
126
- throw new Error('All requests require authentication.');
143
+ async function unifiedAuthenticate(req, options) {
144
+ const auth = await unifiedAuthenticateInternal(req);
145
+ if (options?.usingUaCookieRoutes) {
146
+ options.exceptRoutes ??= new Set();
147
+ options.exceptRoutes.add('/.uaService');
148
+ options.exceptRoutes.add('/.uaRedirect');
149
+ }
150
+ const isAuthenticatedRoute = options?.authenticateAll && (options.exceptRoutes == null || !options.exceptRoutes.has(req.routeOptions.url));
151
+ if (isAuthenticatedRoute) {
152
+ if ((0, txstate_utils_1.isBlank)(auth?.username))
153
+ throw new Error('Request requires authentication.');
154
+ }
127
155
  return auth;
128
156
  }
157
+ /**
158
+ * @deprecated Use unifiedAuthenticateWithOptions with { authenticateAll: true } instead.
159
+ */
160
+ async function unifiedAuthenticateAll(req) {
161
+ return (await unifiedAuthenticate(req, { authenticateAll: true }));
162
+ }
163
+ /**
164
+ * This function is available for server-side view code instead of a client-side application
165
+ * using a framework. It will automatically redirect the user to the Unified Auth login page
166
+ * and return true if they are not authenticated. Otherwise it simply returns false.
167
+ */
129
168
  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`)}`);
169
+ if ((0, txstate_utils_1.isBlank)(req.auth?.username)) {
170
+ const loginUrl = new URL(process.env.UA_URL + '/login');
171
+ loginUrl.searchParams.set('clientId', process.env.UA_CLIENTID);
172
+ 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());
173
+ loginUrl.searchParams.set('requestedUrl', req.originalUrl);
174
+ void res.redirect(loginUrl.toString());
134
175
  return true;
135
176
  }
136
177
  else {
@@ -149,38 +190,53 @@ function registerUaCookieRoutes(app) {
149
190
  }
150
191
  }
151
192
  }, 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');
193
+ const redirectUrl = req.auth?.issuerConfig?.logoutUrl
194
+ ? `${req.auth.issuerConfig.logoutUrl.toString()}?unifiedJwt=${encodeURIComponent(req.auth.token)}`
195
+ : (process.env.PUBLIC_URL || new URL('..', req.url).toString());
155
196
  return res
156
197
  .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])}`);
198
+ .redirect(redirectUrl);
158
199
  });
159
200
  app.get('/.uaService', {
160
201
  schema: {
161
202
  querystring: {
162
203
  type: 'object',
163
204
  properties: {
164
- unifiedJwt: { type: 'string', pattern: '^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$' }
205
+ unifiedJwt: { type: 'string', pattern: '^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$' },
206
+ requestedUrl: { type: 'string', format: 'uri' }
165
207
  },
166
208
  required: ['unifiedJwt'],
167
209
  additionalProperties: false
168
- },
169
- headers: {
210
+ }
211
+ }
212
+ }, async (req, res) => {
213
+ return res
214
+ .header('Set-Cookie', `${exports.uaCookieName}=${req.query.unifiedJwt}; Path=/; Secure; HttpOnly; SameSite=Lax`)
215
+ .redirect(req.query.requestedUrl ?? (process.env.PUBLIC_URL || new URL('..', req.url).toString()));
216
+ });
217
+ /**
218
+ * In the case of a client-side application that uses the UA cookie to authenticate,
219
+ * the client code can detect a 401 from the API and redirect the user to this endpoint.
220
+ *
221
+ * This endpoint will redirect the browser to Unified Auth so that the client code does
222
+ * not need to have any configuration for Unified Auth.
223
+ */
224
+ app.get('/.uaRedirect', {
225
+ schema: {
226
+ querystring: {
170
227
  type: 'object',
171
228
  properties: {
172
- cookie: { type: 'string', pattern: `${exports.uaCookieName}_return=${encodeURIComponent(`${process.env.PUBLIC_URL ?? ''}`)}` }
229
+ requestedUrl: { type: 'string', format: 'uri' }
173
230
  },
174
- required: ['cookie']
231
+ additionalProperties: false
175
232
  }
176
233
  }
177
234
  }, 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]));
235
+ const loginUrl = new URL(process.env.UA_URL + '/login');
236
+ loginUrl.searchParams.set('clientId', process.env.UA_CLIENTID);
237
+ loginUrl.searchParams.set('returnUrl', new URL('.uaService', process.env.PUBLIC_URL || new URL(req.url, req.protocol + '://' + req.hostname)).toString());
238
+ if (req.query.requestedUrl)
239
+ loginUrl.searchParams.set('requestedUrl', req.query.requestedUrl);
240
+ return res.redirect(loginUrl.toString());
185
241
  });
186
242
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify-txstate",
3
- "version": "3.5.1",
3
+ "version": "3.6.0",
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
  },