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.
@@ -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 ADDED
@@ -0,0 +1,49 @@
1
+ import type { FastifyReply, FastifyRequest } from 'fastify';
2
+ import { type FastifyInstanceTyped } from './server.ts';
3
+ export interface IssuerChoice {
4
+ issuerUrl: string;
5
+ redirectHref: string;
6
+ }
7
+ /**
8
+ * Register cookie-based OAuth login/logout endpoints. Uses the authorization code flow
9
+ * with PKCE (S256) to exchange a code for tokens, then stores the ID token in an HttpOnly
10
+ * cookie. The access token and refresh token are stored in separate cookies (optionally
11
+ * encrypted via OAUTH_COOKIE_SECRET) so that the ID token can be transparently refreshed
12
+ * by jwtAuthenticate when it expires, and the access token is available at
13
+ * `req.auth.accessToken` for calling provider APIs.
14
+ *
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).
22
+ *
23
+ * Registers three routes:
24
+ * - `/.oauthRedirect` - Redirects to the OAuth provider's login page. The client passes
25
+ * `requestedUrl` (required) which is sent to the provider as the `state` parameter,
26
+ * round-tripped back, and used as the redirect destination after login.
27
+ * - `/.oauthCallback` - Handles the provider's redirect, exchanges the code for tokens
28
+ * using the PKCE code verifier. Sets the ID token (or JWT access token as fallback),
29
+ * access token, and refresh token as cookies.
30
+ * - `/.oauthLogout` - Clears all OAuth cookies and redirects to the provider's logout
31
+ * endpoint if available.
32
+ */
33
+ export declare function registerOAuthCookieRoutes(app: FastifyInstanceTyped, options?: {
34
+ /** Scopes to always include in the authorization request, merged with any scopes
35
+ * the client passes via the `scope` query parameter. */
36
+ scopes?: string[];
37
+ /** When multiple issuers are configured and the client doesn't specify one,
38
+ * this function is called to render a login selection page. It receives an array
39
+ * of issuer URLs with their corresponding redirect hrefs and should return an HTML
40
+ * string. If not provided, the first trusted issuer is used. */
41
+ loginPage?: (issuers: IssuerChoice[]) => string;
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>;