fastify-txstate 4.0.0 → 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.
@@ -0,0 +1,491 @@
1
+ import { createCipheriv, createDecipheriv, createHash, createPublicKey, createSecretKey, randomBytes } from 'node:crypto';
2
+ import { createRemoteJWKSet, decodeJwt, importJWK, jwtVerify } from 'jose';
3
+ import { Cache, isBlank, isNotBlank, toArray } from 'txstate-utils';
4
+ // Cookie names are owned here; oauth.ts and unified-auth.ts import them so the random
5
+ // fallbacks don't drift between modules in deployments that don't set the env vars.
6
+ export const oauthCookieName = process.env.OAUTH_COOKIE_NAME ?? randomBytes(16).toString('hex');
7
+ export const refreshCookieName = oauthCookieName + '_rt';
8
+ export const accessTokenCookieName = oauthCookieName + '_at';
9
+ export const uaCookieName = process.env.UA_COOKIE_NAME ?? randomBytes(16).toString('hex');
10
+ const oauthCookieRegex = new RegExp(`${oauthCookieName}=([^;]+)`, 'v');
11
+ const refreshCookieRegex = new RegExp(`${refreshCookieName}=([^;]+)`, 'v');
12
+ const accessTokenCookieRegex = new RegExp(`${accessTokenCookieName}=([^;]+)`, 'v');
13
+ const uaCookieRegex = new RegExp(`${uaCookieName}=([^;]+)`, 'v');
14
+ let cookieEncryptionKey;
15
+ if (isNotBlank(process.env.OAUTH_COOKIE_SECRET)) {
16
+ cookieEncryptionKey = createHash('sha256').update(process.env.OAUTH_COOKIE_SECRET).digest();
17
+ }
18
+ export function wrapRefreshToken(token) {
19
+ if (!cookieEncryptionKey)
20
+ return token;
21
+ const iv = randomBytes(12);
22
+ const cipher = createCipheriv('aes-256-gcm', cookieEncryptionKey, iv);
23
+ const encrypted = Buffer.concat([cipher.update(token, 'utf8'), cipher.final()]);
24
+ const tag = cipher.getAuthTag();
25
+ return Buffer.concat([iv, tag, encrypted]).toString('base64url');
26
+ }
27
+ export function unwrapRefreshToken(value) {
28
+ if (!cookieEncryptionKey)
29
+ return value;
30
+ try {
31
+ const data = Buffer.from(value, 'base64url');
32
+ const iv = data.subarray(0, 12);
33
+ const tag = data.subarray(12, 28);
34
+ const encrypted = data.subarray(28);
35
+ const decipher = createDecipheriv('aes-256-gcm', cookieEncryptionKey, iv);
36
+ decipher.setAuthTag(tag);
37
+ return decipher.update(encrypted, undefined, 'utf8') + decipher.final('utf8');
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ let hasInit = false;
44
+ const issuerConfigByIss = new Map();
45
+ const verifyKeyByIss = new Map();
46
+ const issuerInternalUrls = new Map();
47
+ function inferType(config) {
48
+ if (config.type)
49
+ return config.type;
50
+ if (config.iss === 'unified-auth')
51
+ return 'unified-auth';
52
+ if (isNotBlank(config.secret))
53
+ return 'secret';
54
+ if (isNotBlank(config.publicKey))
55
+ return 'publicKey';
56
+ if (isNotBlank(config.url))
57
+ return 'jwks';
58
+ throw new Error(`Could not infer type for JWT issuer ${config.iss}. Set "type" explicitly or provide one of url/publicKey/secret.`);
59
+ }
60
+ export function toInternalUrl(url) {
61
+ for (const [external, internal] of issuerInternalUrls) {
62
+ if (url.startsWith(external))
63
+ return internal + url.slice(external.length);
64
+ }
65
+ return url;
66
+ }
67
+ export function getOAuthIssuerUrls() {
68
+ init();
69
+ const urls = [];
70
+ for (const config of issuerConfigByIss.values()) {
71
+ if (config.type === 'oauth' && config.url)
72
+ urls.push(config.url);
73
+ }
74
+ return urls;
75
+ }
76
+ export function getIssuerConfig(iss) {
77
+ init();
78
+ const config = issuerConfigByIss.get(iss);
79
+ if (!config)
80
+ return undefined;
81
+ return {
82
+ iss: config.iss,
83
+ url: config.url,
84
+ validateUrl: config.validateUrl,
85
+ logoutUrl: config.logoutUrl
86
+ };
87
+ }
88
+ export async function getOAuthDiscovery(issuerUrl) {
89
+ init();
90
+ return await discoveryCache.get(issuerUrl);
91
+ }
92
+ const jwksRemoteCache = new Cache(async (jwksUri) => createRemoteJWKSet(new URL(toInternalUrl(jwksUri))), { freshseconds: 3600 });
93
+ const uaJwkCache = new Cache(async (url) => {
94
+ const { keys } = await (await fetch(toInternalUrl(url))).json();
95
+ const map = {};
96
+ for (const jwk of keys) {
97
+ if (jwk.kid)
98
+ map[jwk.kid] = await importJWK(jwk);
99
+ }
100
+ return map;
101
+ });
102
+ function uaRemoteJWKSet(url) {
103
+ return async (header) => {
104
+ const map = await uaJwkCache.get(url);
105
+ return map[header.kid];
106
+ };
107
+ }
108
+ const discoveryCache = new Cache(async (issuerUrl) => {
109
+ const base = issuerUrl.endsWith('/') ? issuerUrl : issuerUrl + '/';
110
+ for (const path of ['.well-known/openid-configuration', '.well-known/oauth-authorization-server']) {
111
+ try {
112
+ const resp = await fetch(new URL(path, toInternalUrl(base)));
113
+ if (resp.ok) {
114
+ const doc = await resp.json();
115
+ if (isNotBlank(doc.jwks_uri))
116
+ return doc;
117
+ }
118
+ }
119
+ catch { /* try next */ }
120
+ }
121
+ return undefined;
122
+ }, { freshseconds: 3600 });
123
+ function buildLogoutUrl(config, type) {
124
+ if (isNotBlank(config.logoutUrl)) {
125
+ return config.url ? new URL(config.logoutUrl, config.url) : new URL(config.logoutUrl);
126
+ }
127
+ if (type === 'unified-auth') {
128
+ if (isNotBlank(process.env.UA_URL))
129
+ return new URL(process.env.UA_URL + '/logout');
130
+ if (config.url)
131
+ return new URL('logout', config.url);
132
+ }
133
+ return undefined;
134
+ }
135
+ function csvEnv(value) {
136
+ return value?.split(',').map(s => s.trim()).filter(isNotBlank) ?? [];
137
+ }
138
+ function issuersFromEnv() {
139
+ const result = [];
140
+ if (isNotBlank(process.env.UA_URL)) {
141
+ result.push({
142
+ iss: 'unified-auth',
143
+ type: 'unified-auth',
144
+ url: process.env.UA_URL,
145
+ internalUrl: isNotBlank(process.env.UA_URL_INTERNAL) ? process.env.UA_URL_INTERNAL : undefined
146
+ });
147
+ }
148
+ if (isNotBlank(process.env.OAUTH_URLS)) {
149
+ const internalByExternal = new Map();
150
+ for (const pair of csvEnv(process.env.OAUTH_INTERNAL_URLS)) {
151
+ const eq = pair.indexOf('=');
152
+ if (eq > 0)
153
+ internalByExternal.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
154
+ }
155
+ for (const url of csvEnv(process.env.OAUTH_URLS)) {
156
+ result.push({
157
+ iss: url,
158
+ type: 'oauth',
159
+ url,
160
+ internalUrl: internalByExternal.get(url)
161
+ });
162
+ }
163
+ }
164
+ if (isNotBlank(process.env.JWT_SECRET)) {
165
+ result.push({
166
+ iss: 'jwt-secret',
167
+ type: 'secret',
168
+ secret: process.env.JWT_SECRET
169
+ });
170
+ }
171
+ if (isNotBlank(process.env.JWT_PUBLIC_KEY)) {
172
+ // accept PEM with literal \n escapes for env-var friendliness
173
+ result.push({
174
+ iss: 'jwt-public-key',
175
+ type: 'publicKey',
176
+ publicKey: process.env.JWT_PUBLIC_KEY.replace(/\\n/gv, '\n')
177
+ });
178
+ }
179
+ return result;
180
+ }
181
+ export function init() {
182
+ if (hasInit)
183
+ return;
184
+ hasInit = true;
185
+ const globalAudiences = csvEnv(process.env.JWT_TRUSTED_AUDIENCES);
186
+ const globalClientIds = csvEnv(process.env.JWT_TRUSTED_CLIENTIDS);
187
+ const jsonIssuers = process.env.JWT_TRUSTED_ISSUERS
188
+ ? toArray(JSON.parse(process.env.JWT_TRUSTED_ISSUERS))
189
+ : [];
190
+ // env-derived issuers first, then JSON-derived — JSON entries with the same iss override.
191
+ for (const issuer of [...issuersFromEnv(), ...jsonIssuers]) {
192
+ const type = inferType(issuer);
193
+ if (isNotBlank(issuer.url) && isNotBlank(issuer.internalUrl)) {
194
+ issuerInternalUrls.set(issuer.url, issuer.internalUrl);
195
+ }
196
+ switch (type) {
197
+ case 'unified-auth':
198
+ if (!issuer.url)
199
+ throw new Error(`unified-auth issuer ${issuer.iss} requires url`);
200
+ verifyKeyByIss.set(issuer.iss, uaRemoteJWKSet(issuer.url));
201
+ break;
202
+ case 'oauth':
203
+ if (!issuer.url)
204
+ throw new Error(`oauth issuer ${issuer.iss} requires url`);
205
+ // verify key resolved per request via discovery
206
+ break;
207
+ case 'jwks':
208
+ if (!issuer.url)
209
+ throw new Error(`jwks issuer ${issuer.iss} requires url`);
210
+ verifyKeyByIss.set(issuer.iss, createRemoteJWKSet(new URL(toInternalUrl(issuer.url))));
211
+ break;
212
+ case 'publicKey':
213
+ if (!issuer.publicKey)
214
+ throw new Error(`publicKey issuer ${issuer.iss} requires publicKey`);
215
+ verifyKeyByIss.set(issuer.iss, createPublicKey(issuer.publicKey));
216
+ break;
217
+ case 'secret':
218
+ if (!issuer.secret)
219
+ throw new Error(`secret issuer ${issuer.iss} requires secret`);
220
+ verifyKeyByIss.set(issuer.iss, createSecretKey(Buffer.from(issuer.secret, 'ascii')));
221
+ break;
222
+ }
223
+ const validateUrl = type === 'unified-auth'
224
+ ? (isNotBlank(issuer.validateUrl) ? new URL(issuer.validateUrl, issuer.url) : new URL('validateToken', issuer.url))
225
+ : undefined;
226
+ const audiences = new Set([...(issuer.audiences ?? []), ...globalAudiences]);
227
+ const clientIds = new Set([...(issuer.clientIds ?? []), ...globalClientIds]);
228
+ issuerConfigByIss.set(issuer.iss, {
229
+ type,
230
+ iss: issuer.iss,
231
+ url: issuer.url,
232
+ validateUrl,
233
+ logoutUrl: buildLogoutUrl(issuer, type),
234
+ audiences: audiences.size ? audiences : undefined,
235
+ clientIds: clientIds.size ? clientIds : undefined
236
+ });
237
+ }
238
+ }
239
+ function checkAudience(aud, audiences, req) {
240
+ if (!audiences?.size)
241
+ return true;
242
+ // RFC 7519: a consumer accepts a token if it identifies itself in the aud claim, so
243
+ // any single trusted audience appearing in the token is sufficient — additional
244
+ // untrusted audiences alongside it do not cause rejection.
245
+ if (!toArray(aud).some(a => audiences.has(a))) {
246
+ req.log.warn(`Received token with untrusted audience: ${String(aud)}`);
247
+ return false;
248
+ }
249
+ return true;
250
+ }
251
+ function checkClientId(clientId, clientIds, req) {
252
+ if (!clientIds?.size)
253
+ return true;
254
+ if (!clientIds.has(clientId)) {
255
+ req.log.warn(`Received token with untrusted client_id: ${String(clientId)}`);
256
+ return false;
257
+ }
258
+ return true;
259
+ }
260
+ async function resolveVerifyKey(config) {
261
+ if (config.type === 'oauth') {
262
+ if (!config.url)
263
+ return undefined;
264
+ const discovery = await discoveryCache.get(config.url);
265
+ if (!discovery?.jwks_uri)
266
+ return undefined;
267
+ return { key: await jwksRemoteCache.get(discovery.jwks_uri), discovery };
268
+ }
269
+ const key = verifyKeyByIss.get(config.iss);
270
+ return key ? { key } : undefined;
271
+ }
272
+ const tokenCache = new Cache(async (token, req) => {
273
+ const claims = decodeJwt(token);
274
+ if (!claims.iss) {
275
+ req.log.warn('Received token without an issuer claim.');
276
+ return undefined;
277
+ }
278
+ const config = issuerConfigByIss.get(claims.iss);
279
+ if (!config) {
280
+ req.log.warn(`Received token with untrusted issuer: ${claims.iss}`);
281
+ return undefined;
282
+ }
283
+ try {
284
+ const resolved = await resolveVerifyKey(config);
285
+ if (!resolved) {
286
+ req.log.warn(`Could not resolve verification key for issuer ${claims.iss}`);
287
+ return undefined;
288
+ }
289
+ const { payload } = await jwtVerify(token, resolved.key);
290
+ if (!checkAudience(payload.aud, config.audiences, req))
291
+ return undefined;
292
+ if (!checkClientId(payload.client_id, config.clientIds, req))
293
+ return undefined;
294
+ return { payload, config, discovery: resolved.discovery };
295
+ }
296
+ catch (e) {
297
+ const code = e.code;
298
+ if (code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' && code !== 'ERR_JWT_EXPIRED')
299
+ req.log.error(e);
300
+ return undefined;
301
+ }
302
+ }, { freshseconds: 3600 });
303
+ const validateCache = new Cache(async (token, payload) => {
304
+ const config = payload.iss ? issuerConfigByIss.get(payload.iss) : undefined;
305
+ if (!config?.validateUrl)
306
+ return;
307
+ // avoid checking for deauth until the token is more than 5 minutes old
308
+ if (new Date(payload.iat * 1000) > new Date(Date.now() - 1000 * 60 * 5))
309
+ return;
310
+ const validateUrl = new URL(config.validateUrl);
311
+ validateUrl.searchParams.set('unifiedJwt', token);
312
+ const resp = await fetch(validateUrl);
313
+ const validate = await resp.json();
314
+ if (!validate.valid)
315
+ 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.');
316
+ });
317
+ async function tryRefresh(req, expiredIss) {
318
+ const m = req.headers.cookie?.match(refreshCookieRegex);
319
+ if (!m)
320
+ return undefined;
321
+ const refreshToken = unwrapRefreshToken(m[1]);
322
+ if (!refreshToken)
323
+ return undefined;
324
+ const clientId = process.env.OAUTH_COOKIE_CLIENT_ID;
325
+ if (!clientId)
326
+ return undefined;
327
+ const candidate = (expiredIss && issuerConfigByIss.get(expiredIss)?.type === 'oauth')
328
+ ? issuerConfigByIss.get(expiredIss)
329
+ : [...issuerConfigByIss.values()].find(c => c.type === 'oauth');
330
+ if (!candidate?.url)
331
+ return undefined;
332
+ const discovery = await discoveryCache.get(candidate.url);
333
+ if (!discovery?.token_endpoint)
334
+ return undefined;
335
+ const body = {
336
+ grant_type: 'refresh_token',
337
+ refresh_token: refreshToken,
338
+ client_id: clientId
339
+ };
340
+ const clientSecret = process.env.OAUTH_COOKIE_CLIENT_SECRET;
341
+ if (clientSecret)
342
+ body.client_secret = clientSecret;
343
+ try {
344
+ const tokenResp = await fetch(toInternalUrl(discovery.token_endpoint), {
345
+ method: 'POST',
346
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
347
+ body: new URLSearchParams(body)
348
+ });
349
+ if (!tokenResp.ok)
350
+ return undefined;
351
+ const tokens = await tokenResp.json();
352
+ if (!tokens.id_token)
353
+ return undefined;
354
+ req.pendingOAuthCookies = [
355
+ `${oauthCookieName}=${tokens.id_token}; Path=/; Secure; HttpOnly; SameSite=Lax`
356
+ ];
357
+ if (tokens.access_token) {
358
+ req.pendingOAuthCookies.push(`${accessTokenCookieName}=${wrapRefreshToken(tokens.access_token)}; Path=/; Secure; HttpOnly; SameSite=Lax`);
359
+ }
360
+ if (tokens.refresh_token) {
361
+ req.pendingOAuthCookies.push(`${refreshCookieName}=${wrapRefreshToken(tokens.refresh_token)}; Path=/; Secure; HttpOnly; SameSite=Lax`);
362
+ }
363
+ const cached = await tokenCache.get(tokens.id_token, req);
364
+ if (!cached)
365
+ return undefined;
366
+ if (cached.payload.exp && cached.payload.exp * 1000 <= Date.now())
367
+ return undefined;
368
+ return { token: tokens.id_token, payload: cached.payload, config: cached.config, discovery: cached.discovery };
369
+ }
370
+ catch (e) {
371
+ req.log.error(e);
372
+ return undefined;
373
+ }
374
+ }
375
+ function tokenFromReq(req) {
376
+ const m = req.headers.authorization?.match(/^bearer (.*)$/iv);
377
+ if (m)
378
+ return m[1];
379
+ const oauthM = req.headers.cookie?.match(oauthCookieRegex);
380
+ if (oauthM)
381
+ return oauthM[1];
382
+ const uaM = req.headers.cookie?.match(uaCookieRegex);
383
+ if (uaM)
384
+ return uaM[1];
385
+ }
386
+ function accessTokenFromReq(req) {
387
+ const m = req.headers.cookie?.match(accessTokenCookieRegex);
388
+ if (!m)
389
+ return undefined;
390
+ return unwrapRefreshToken(m[1]);
391
+ }
392
+ function authIssuerConfig(config, discovery) {
393
+ const logoutUrl = config.type === 'oauth' && isNotBlank(discovery?.end_session_endpoint)
394
+ ? new URL(discovery.end_session_endpoint)
395
+ : config.logoutUrl;
396
+ return {
397
+ iss: config.iss,
398
+ url: config.url,
399
+ validateUrl: config.validateUrl,
400
+ logoutUrl
401
+ };
402
+ }
403
+ function buildAuthInfo(result, extraClaims) {
404
+ const { token, payload, config, discovery } = result;
405
+ return {
406
+ ...extraClaims?.(payload),
407
+ token,
408
+ issuerConfig: authIssuerConfig(config, discovery),
409
+ username: payload.sub,
410
+ sessionId: payload.sub + '-' + String(payload.iat),
411
+ sessionCreatedAt: payload.iat ? new Date(payload.iat * 1000) : undefined,
412
+ clientId: payload.client_id,
413
+ impersonatedBy: payload.act?.sub,
414
+ scope: payload.scope
415
+ };
416
+ }
417
+ async function jwtAuthenticateInternal(req, extraClaims) {
418
+ init();
419
+ const token = tokenFromReq(req);
420
+ if (!token)
421
+ return undefined;
422
+ let result;
423
+ const cached = await tokenCache.get(token, req);
424
+ if (!cached) {
425
+ // jwtVerify rejects expired tokens — try a refresh if we have a refresh-token cookie
426
+ try {
427
+ const { iss } = decodeJwt(token);
428
+ result = await tryRefresh(req, iss ?? undefined);
429
+ }
430
+ catch { /* not a JWT */ }
431
+ }
432
+ else if (cached.payload.exp && cached.payload.exp * 1000 <= Date.now()) {
433
+ // belt-and-suspenders: catch tokens that expired between cache and now
434
+ result = await tryRefresh(req, cached.payload.iss);
435
+ }
436
+ else {
437
+ result = { token, payload: cached.payload, config: cached.config, discovery: cached.discovery };
438
+ }
439
+ if (!result)
440
+ return undefined;
441
+ await validateCache.get(result.token, result.payload);
442
+ const authInfo = buildAuthInfo(result, extraClaims);
443
+ authInfo.accessToken = accessTokenFromReq(req);
444
+ return authInfo;
445
+ }
446
+ // Routes contributed by registerOAuthCookieRoutes / registerUaCookieRoutes. The
447
+ // authenticator returned by jwtAuthenticate consults these at request time so that
448
+ // registration order (factory vs. route registration) doesn't matter.
449
+ export const registeredExceptRoutes = new Set();
450
+ export const registeredOptionalRoutes = new Set();
451
+ /**
452
+ * Build an `authenticate` function that validates JWTs from the Authorization Bearer
453
+ * header or a session cookie. Supports any mix of issuer types via the
454
+ * JWT_TRUSTED_ISSUERS env var:
455
+ *
456
+ * - 'oauth' — OAuth/OIDC provider with .well-known auto-discovery
457
+ * - 'jwks' — JWKS endpoint URL (no discovery)
458
+ * - 'unified-auth' — TxState Unified Auth (JWKS + /validateToken poll for deauth)
459
+ * - 'publicKey' — PEM-encoded asymmetric public key
460
+ * - 'secret' — symmetric HMAC secret
461
+ *
462
+ * Usage:
463
+ * new Server({ authenticate: jwtAuthenticate({ authenticateAll: true }) })
464
+ *
465
+ * Or with no options:
466
+ * new Server({ authenticate: jwtAuthenticate() })
467
+ *
468
+ * Calling `registerOAuthCookieRoutes` or `registerUaCookieRoutes` automatically excludes
469
+ * their callback/redirect routes from authentication requirements and marks their logout
470
+ * routes as optional, so you do not need to configure that here.
471
+ *
472
+ * If a refresh-token cookie is present (set by registerOAuthCookieRoutes) and the access
473
+ * token has expired, the returned authenticator transparently exchanges the refresh
474
+ * token for a new access token and queues the replacement cookies on
475
+ * `req.pendingOAuthCookies`. The onSend hook installed by registerOAuthCookieRoutes
476
+ * flushes those cookies onto the response.
477
+ */
478
+ export function jwtAuthenticate(options) {
479
+ const exceptRoutes = new Set(options?.exceptRoutes);
480
+ const optionalRoutes = new Set(options?.optionalRoutes);
481
+ return async (req) => {
482
+ const url = req.routeOptions.url;
483
+ if (exceptRoutes.has(url) || registeredExceptRoutes.has(url))
484
+ return undefined;
485
+ const auth = await jwtAuthenticateInternal(req, options?.extraClaims);
486
+ if (options?.authenticateAll && !optionalRoutes.has(url) && !registeredOptionalRoutes.has(url) && isBlank(auth?.username)) {
487
+ throw new Error('Request requires authentication.');
488
+ }
489
+ return auth;
490
+ };
491
+ }
package/lib/oauth.d.ts CHANGED
@@ -1,49 +1,24 @@
1
- import type { FastifyRequest } from 'fastify';
2
- import { type FastifyTxStateAuthInfo, type FastifyInstanceTyped } from './server.ts';
3
- declare module 'fastify' {
4
- interface FastifyRequest {
5
- pendingOAuthCookies?: string[];
6
- }
1
+ import type { FastifyReply, FastifyRequest } from 'fastify';
2
+ import { type FastifyInstanceTyped } from './server.ts';
3
+ export interface IssuerChoice {
4
+ issuerUrl: string;
5
+ redirectHref: string;
7
6
  }
8
- /**
9
- * Authenticate requests using JWT tokens from any OAuth/OIDC provider. The token's
10
- * issuer claim is used to auto-discover the provider's JWKS endpoint for signature
11
- * verification.
12
- *
13
- * Expects JWT tokens (access tokens or ID tokens) in the Authorization Bearer header
14
- * or in a cookie set by registerOAuthCookieRoutes.
15
- *
16
- * For providers like Google that issue opaque access tokens, have the client send the
17
- * ID token instead — it's a standard JWT that proves the user's identity without
18
- * requiring a round-trip to the provider on every request.
19
- */
20
- export declare function oauthAuthenticate(req: FastifyRequest, options?: {
21
- /** If true, all requests require authentication, except routes listed in exceptRoutes or optionalRoutes. */
22
- authenticateAll?: boolean;
23
- /** Routes that skip authentication entirely. They will not receive an auth object. */
24
- exceptRoutes?: Set<string>;
25
- /** Routes that do not require authentication, but will fill req.auth if a session is available. */
26
- optionalRoutes?: Set<string>;
27
- /** Set this true if you are using registerOAuthCookieRoutes and authenticateAll. */
28
- usingOAuthCookieRoutes?: boolean;
29
- /** Receives the full JWT payload and returns extra properties to merge into the auth object.
30
- * If you use this, you should also set OAUTH_TRUSTED_AUDIENCES to prevent tokens from
31
- * other applications carrying unexpected authorization claims. */
32
- extraClaims?: (payload: Record<string, unknown>) => Record<string, unknown>;
33
- }): Promise<FastifyTxStateAuthInfo | undefined>;
34
7
  /**
35
8
  * Register cookie-based OAuth login/logout endpoints. Uses the authorization code flow
36
9
  * with PKCE (S256) to exchange a code for tokens, then stores the ID token in an HttpOnly
37
10
  * cookie. The access token and refresh token are stored in separate cookies (optionally
38
11
  * encrypted via OAUTH_COOKIE_SECRET) so that the ID token can be transparently refreshed
39
- * when it expires and the access token is available at `req.auth.accessToken` for calling
40
- * provider APIs.
12
+ * by jwtAuthenticate when it expires, and the access token is available at
13
+ * `req.auth.accessToken` for calling provider APIs.
41
14
  *
42
- * Requires OAUTH_CLIENT_ID environment variable. OAUTH_CLIENT_SECRET is optional — PKCE
43
- * provides the security for the code exchange, but some providers require a client secret
44
- * even with PKCE. OAUTH_COOKIE_SECRET is optional — if set, the access token and refresh
45
- * token cookies are encrypted with AES-256-GCM; if not, they are stored as plaintext
46
- * (still HttpOnly and Secure).
15
+ * Requires OAUTH_COOKIE_CLIENT_ID environment variable. OAUTH_COOKIE_CLIENT_SECRET is
16
+ * optional — PKCE provides the security for the code exchange, but some providers require
17
+ * a client secret even with PKCE. OAUTH_COOKIE_SECRET is optional — if set, the access
18
+ * token and refresh token cookies are encrypted with AES-256-GCM; if not, they are stored
19
+ * as plaintext (still HttpOnly and Secure).
20
+ *
21
+ * Trusted issuers are configured via OAUTH_URLS or JWT_TRUSTED_ISSUERS (see jwt-auth.ts).
47
22
  *
48
23
  * Registers three routes:
49
24
  * - `/.oauthRedirect` - Redirects to the OAuth provider's login page. The client passes
@@ -55,10 +30,6 @@ export declare function oauthAuthenticate(req: FastifyRequest, options?: {
55
30
  * - `/.oauthLogout` - Clears all OAuth cookies and redirects to the provider's logout
56
31
  * endpoint if available.
57
32
  */
58
- export interface IssuerChoice {
59
- issuerUrl: string;
60
- redirectHref: string;
61
- }
62
33
  export declare function registerOAuthCookieRoutes(app: FastifyInstanceTyped, options?: {
63
34
  /** Scopes to always include in the authorization request, merged with any scopes
64
35
  * the client passes via the `scope` query parameter. */
@@ -69,3 +40,10 @@ export declare function registerOAuthCookieRoutes(app: FastifyInstanceTyped, opt
69
40
  * string. If not provided, the first trusted issuer is used. */
70
41
  loginPage?: (issuers: IssuerChoice[]) => string;
71
42
  }): void;
43
+ /**
44
+ * This function is available for server-side view code instead of a client-side application
45
+ * using a framework. It will automatically redirect the user through the OAuth login flow
46
+ * (via /.oauthRedirect, which must be registered by registerOAuthCookieRoutes) and return
47
+ * true if they are not authenticated. Otherwise it simply returns false.
48
+ */
49
+ export declare function requireCookieAuthOAuth(req: FastifyRequest, res: FastifyReply): Promise<boolean>;