@better-auth/oauth-provider 1.7.0-rc.5 → 1.7.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.
@@ -0,0 +1,2551 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
+ import { A as collectExtensionAccessTokenClaims, C as storeToken, E as validateClientCredentials, F as getExtensionGrantHandler, L as getSupportedGrantTypes, M as collectExtensionUserInfoClaims, R as hasUserInfoClaimExtension, T as toResourceList, a as getClient, b as resolveSubjectIdentifier, c as getStoredToken, h as parseClientMetadata, i as extractClientCredentials, j as collectExtensionIdTokenClaims, l as isPKCERequired, n as decryptStoredClientSecret, o as getJwtPlugin, p as normalizeTimestampValue, r as destructureCredentials, t as clientAllowsGrant, w as toAudienceClaim, y as resolveSessionAuthTime } from "./utils-CCi1maVd.mjs";
3
+ import { APIError } from "better-auth/api";
4
+ import { generateRandomString, symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
5
+ import { APIError as APIError$1 } from "better-call";
6
+ import { logger } from "@better-auth/core/env";
7
+ import * as z from "zod";
8
+ import { createDpopReplayStore, enforceDpopBinding, generateCodeChallenge, getConfirmationJkt, getDpopJktFromPayload, getJwks, isDpopBindingError, isDpopProofError, parseAccessTokenAuthorization, stripAccessTokenAuthorizationScheme, verifyDpopProof } from "better-auth/oauth2";
9
+ import { SignJWT, base64url, createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
10
+ import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
11
+ import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
12
+ //#region src/authentication-context.ts
13
+ const RESERVED_ID_TOKEN_CLAIMS = new Set([
14
+ "iss",
15
+ "sub",
16
+ "aud",
17
+ "exp",
18
+ "nbf",
19
+ "iat",
20
+ "jti",
21
+ "auth_time",
22
+ "nonce",
23
+ "acr",
24
+ "amr",
25
+ "azp",
26
+ "sid",
27
+ "at_hash",
28
+ "c_hash",
29
+ "s_hash"
30
+ ]);
31
+ /**
32
+ * Removes provider-owned ID token claim names from custom claims.
33
+ *
34
+ * `customIdTokenClaims` is an extension point for additional claims. It must not
35
+ * replace the issuer, subject, audience, lifetime, nonce, authorized party,
36
+ * token-binding hashes, session binding, or authentication context that the
37
+ * provider owns.
38
+ */
39
+ function stripReservedIdTokenClaims(claims) {
40
+ if (!claims) return {};
41
+ const stripped = [];
42
+ const safeClaims = Object.create(null);
43
+ for (const [key, value] of Object.entries(claims)) {
44
+ if (RESERVED_ID_TOKEN_CLAIMS.has(key)) {
45
+ stripped.push(key);
46
+ continue;
47
+ }
48
+ safeClaims[key] = value;
49
+ }
50
+ if (stripped.length > 0) logger.warn(`oauth-provider: stripped reserved id-token claim name(s): ${stripped.join(", ")}. The AS owns these claim values.`);
51
+ return safeClaims;
52
+ }
53
+ //#endregion
54
+ //#region src/claims-request.ts
55
+ const claimRequestMemberSchema = z.record(z.string(), z.unknown()).nullable();
56
+ const acrClaimRequestMemberSchema = z.looseObject({
57
+ essential: z.boolean().optional(),
58
+ value: z.string().optional(),
59
+ values: z.array(z.string()).optional()
60
+ }).nullable();
61
+ const idTokenClaimsRequestSchema = z.object({ acr: acrClaimRequestMemberSchema.optional() }).catchall(claimRequestMemberSchema);
62
+ const oidcClaimsRequestObjectSchema = z.looseObject({
63
+ userinfo: z.record(z.string(), claimRequestMemberSchema).optional(),
64
+ id_token: idTokenClaimsRequestSchema.optional()
65
+ });
66
+ function parseClaimsRequestValue(value) {
67
+ if (typeof value === "string") try {
68
+ return JSON.parse(value);
69
+ } catch {
70
+ return;
71
+ }
72
+ return value;
73
+ }
74
+ function parseOidcClaimsRequestObject(value) {
75
+ const parsed = parseClaimsRequestValue(value);
76
+ const result = oidcClaimsRequestObjectSchema.safeParse(parsed);
77
+ return result.success ? result.data : void 0;
78
+ }
79
+ const claimsRequestParameterValueSchema = z.union([z.string(), z.record(z.string(), z.unknown())]);
80
+ const claimsRequestInputSchema = claimsRequestParameterValueSchema;
81
+ const claimsRequestParameterSchema = claimsRequestParameterValueSchema.refine((value) => parseOidcClaimsRequestObject(value) !== void 0, { error: "claims must be a valid Claims request object" });
82
+ function isValidOidcClaimsRequest(value) {
83
+ return value === void 0 || parseOidcClaimsRequestObject(value) !== void 0;
84
+ }
85
+ function getRequestedUserInfoClaims(value, supportedClaims) {
86
+ const userInfoClaims = parseOidcClaimsRequestObject(value)?.userinfo;
87
+ if (!userInfoClaims) return [];
88
+ const names = Object.keys(userInfoClaims);
89
+ if (!supportedClaims) return names;
90
+ const allowed = new Set(supportedClaims);
91
+ return names.filter((name) => allowed.has(name));
92
+ }
93
+ function canSatisfyEssentialAcrRequest(value, currentAcr) {
94
+ const claimsRequest = parseOidcClaimsRequestObject(value);
95
+ if (!claimsRequest) return value === void 0;
96
+ const acrRequest = claimsRequest.id_token?.acr;
97
+ if (!acrRequest || acrRequest.essential !== true) return true;
98
+ const valueMatches = acrRequest.value === void 0 || acrRequest.value === currentAcr;
99
+ const valuesMatch = acrRequest.values === void 0 || acrRequest.values.includes(currentAcr);
100
+ return valueMatches && valuesMatch;
101
+ }
102
+ function filterClaimsRequestUserInfoClaims(value, allowedUserInfoClaims) {
103
+ const claimsRequest = parseOidcClaimsRequestObject(value);
104
+ if (!claimsRequest) return void 0;
105
+ const allowedClaimSet = new Set(allowedUserInfoClaims);
106
+ const userInfoClaims = Object.fromEntries(Object.entries(claimsRequest.userinfo ?? {}).filter(([claim]) => allowedClaimSet.has(claim)));
107
+ const filteredClaimsRequest = Object.keys(userInfoClaims).length ? {
108
+ ...claimsRequest,
109
+ userinfo: userInfoClaims
110
+ } : Object.fromEntries(Object.entries(claimsRequest).filter(([key]) => key !== "userinfo"));
111
+ return Object.keys(filteredClaimsRequest).length ? filteredClaimsRequest : void 0;
112
+ }
113
+ //#endregion
114
+ //#region src/standard-claims.ts
115
+ function splitDisplayName(name) {
116
+ const parts = name.split(" ").filter((part) => part !== "");
117
+ if (parts.length <= 1) return {};
118
+ return {
119
+ given: parts.slice(0, -1).join(" "),
120
+ family: parts.at(-1)
121
+ };
122
+ }
123
+ /**
124
+ * The OIDC Standard Claims (OIDC Core §5.1) that Better Auth resolves from its
125
+ * own user model, each paired with the scope that requests it (§5.4).
126
+ *
127
+ * This is the single source for UserInfo scope-claim resolution, individual
128
+ * `claims.userinfo` resolution, the discovery `claims_supported` advertisement,
129
+ * and the bound on requested claim names. Adding a standard claim Better Auth
130
+ * can resolve means adding one entry here, not editing UserInfo, the metadata,
131
+ * and the plugin init separately.
132
+ *
133
+ * These claims are delivered only at the UserInfo endpoint, never the ID token:
134
+ * the authorization code flow always issues an access token, so §5.4 routes
135
+ * scope-requested claims to UserInfo. Deployments that support further standard
136
+ * profile claims (such as `birthdate` or `zoneinfo`) supply them through
137
+ * `customUserInfoClaims` and advertise them in `claims_supported`.
138
+ */
139
+ const STANDARD_CLAIMS = {
140
+ name: {
141
+ scope: "profile",
142
+ resolve: (user) => user.name ?? void 0
143
+ },
144
+ picture: {
145
+ scope: "profile",
146
+ resolve: (user) => user.image ?? void 0
147
+ },
148
+ given_name: {
149
+ scope: "profile",
150
+ resolve: (user) => splitDisplayName(user.name).given
151
+ },
152
+ family_name: {
153
+ scope: "profile",
154
+ resolve: (user) => splitDisplayName(user.name).family
155
+ },
156
+ email: {
157
+ scope: "email",
158
+ resolve: (user) => user.email ?? void 0
159
+ },
160
+ email_verified: {
161
+ scope: "email",
162
+ resolve: (user) => user.emailVerified ?? false
163
+ }
164
+ };
165
+ const STANDARD_CLAIM_NAMES = Object.keys(STANDARD_CLAIMS);
166
+ /**
167
+ * The claim names this provider advertises it can supply (discovery
168
+ * `claims_supported`): the operator's `advertisedMetadata.claims_supported`
169
+ * override when set, otherwise the scope-derived standard set computed at plugin
170
+ * init. Used both to advertise and to bound requested `claims.userinfo` names so
171
+ * a client cannot pin arbitrary attacker-controlled strings onto stored tokens.
172
+ */
173
+ function getSupportedClaims(opts) {
174
+ return opts.advertisedMetadata?.claims_supported ?? opts.claims ?? [];
175
+ }
176
+ //#endregion
177
+ //#region src/claims.ts
178
+ /**
179
+ * Claim names the authorization server owns on a JWT access token, which no
180
+ * claim source (the `customAccessTokenClaims` plugin option, an extension
181
+ * contributor, or a resource row's `customClaims`) may override. The AS is the
182
+ * only source of truth for issuer identity, subject, audience, lifetime, scope,
183
+ * authentication context, the token's stable ID, and its sender-constraint
184
+ * (`cnf`).
185
+ *
186
+ * @see RFC 9068 §2.2 (registered access-token claims)
187
+ * @see RFC 7800 / RFC 9449 §6 (`cnf` confirmation — the token's bound key)
188
+ */
189
+ const RESERVED_ACCESS_TOKEN_CLAIMS = new Set([
190
+ "iss",
191
+ "sub",
192
+ "aud",
193
+ "exp",
194
+ "iat",
195
+ "jti",
196
+ "client_id",
197
+ "scope",
198
+ "auth_time",
199
+ "acr",
200
+ "amr",
201
+ "cnf"
202
+ ]);
203
+ /**
204
+ * Returns a copy of `claims` with reserved AS-owned names removed. Emits a
205
+ * `warn` naming the stripped keys when any were present (never silently
206
+ * dropped: surfacing the override attempt matters more than minimizing log
207
+ * noise). Stable iteration order (`Object.entries`) is preserved so token-debug
208
+ * logs stay reproducible across runs.
209
+ */
210
+ function stripReservedClaims(claims) {
211
+ if (!claims) return {};
212
+ const stripped = [];
213
+ const safe = {};
214
+ for (const [key, value] of Object.entries(claims)) {
215
+ if (RESERVED_ACCESS_TOKEN_CLAIMS.has(key)) {
216
+ stripped.push(key);
217
+ continue;
218
+ }
219
+ safe[key] = value;
220
+ }
221
+ if (stripped.length > 0) logger.warn(`oauth-provider: stripped reserved access-token claim name(s): ${stripped.join(", ")}. The AS owns these claim values.`);
222
+ return safe;
223
+ }
224
+ /**
225
+ * The single authority for the enriched (non-AS-owned) claim set an access
226
+ * token carries. Both the JWT mint and the opaque-token introspection
227
+ * re-derive path call this function, so the two formats cannot drift, and
228
+ * reserved RFC 9068 names are stripped unconditionally here so no caller can
229
+ * forget to.
230
+ *
231
+ * Precedence, lowest to highest: extension contributors < per-issuance
232
+ * `accessTokenClaims` < plugin `customAccessTokenClaims` < per-resource
233
+ * `customClaims`. Reserved names are removed from the merged result.
234
+ *
235
+ * Returns only the enriched claims; the caller stamps the AS-owned claims
236
+ * (`iss`/`sub`/`aud`/`exp`/`iat`/`jti`/`client_id`/`scope`/...) itself, so they
237
+ * always win.
238
+ */
239
+ async function resolveAccessTokenClaims(input) {
240
+ const { ctx, opts, user, client, scopes, resources, referenceId, metadata, grantType, sessionId, perRequestClaims, resourcePolicyClaims } = input;
241
+ const extensionClaims = await collectExtensionAccessTokenClaims(opts, {
242
+ ctx,
243
+ opts,
244
+ user,
245
+ client,
246
+ scopes,
247
+ grantType,
248
+ referenceId,
249
+ sessionId,
250
+ resources,
251
+ metadata
252
+ });
253
+ const pluginClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
254
+ user,
255
+ scopes,
256
+ resources,
257
+ referenceId,
258
+ metadata
259
+ }) : {};
260
+ return stripReservedClaims({
261
+ ...extensionClaims,
262
+ ...perRequestClaims ?? {},
263
+ ...pluginClaims,
264
+ ...resourcePolicyClaims
265
+ });
266
+ }
267
+ //#endregion
268
+ //#region src/resources.ts
269
+ /**
270
+ * Source-of-truth list of asymmetric JWS algorithms supported by the JWT
271
+ * plugin. Mirrors the {@link JWSAlgorithms} literal-union type from
272
+ * `packages/better-auth/src/plugins/jwt/types.ts`. Kept as a runtime const
273
+ * so the admin-CRUD zod schema AND the seed-config validator can reject
274
+ * bad values up-front rather than surfacing opaque jose errors at issuance.
275
+ *
276
+ * MUST stay in sync with `JWSAlgorithms`. The type-level guard immediately
277
+ * below fails the typecheck if the two drift.
278
+ */
279
+ const JWS_ALGORITHMS = [
280
+ "EdDSA",
281
+ "ES256",
282
+ "ES512",
283
+ "PS256",
284
+ "RS256"
285
+ ];
286
+ const JWS_ALGORITHM_SET = new Set(JWS_ALGORITHMS);
287
+ /**
288
+ * Upper bound on how many entries are accepted in a JWT's `aud` claim during
289
+ * introspection / revocation validation. Without a cap, a hostile or replayed
290
+ * token can supply hundreds of fake resource identifiers and amplify load on
291
+ * the resource table (one DB lookup per unique unrecognized entry). Real-world
292
+ * deployments use single-digit resource lists; 64 is a generous ceiling that
293
+ * stays well clear of any legitimate use and bounds the per-request DB fan-out.
294
+ *
295
+ * RFC 7519 §4.1.3 does not specify a maximum, so this is a defensive limit,
296
+ * not a spec one. Tokens that exceed it are treated as invalid (introspect →
297
+ * `active: false`, revoke → no-op) rather than triggering the lookup path.
298
+ *
299
+ * @internal
300
+ */
301
+ const MAX_AUD_VALUES = 64;
302
+ /**
303
+ * Validates a resource `identifier` per RFC 8707 §2 (absolute URI, no
304
+ * fragment), honoring {@link OAuthOptions.identifierValidator} when provided.
305
+ *
306
+ * Returns a structured result so callers can decide whether to throw
307
+ * (admin CRUD / DCR) or warn-and-skip (config seed path).
308
+ *
309
+ * @internal
310
+ */
311
+ async function checkIdentifier(opts, identifier) {
312
+ const customValidator = opts.identifierValidator;
313
+ if (customValidator) {
314
+ if (!await customValidator(identifier)) return {
315
+ ok: false,
316
+ reason: `resource identifier ${identifier} failed validation`
317
+ };
318
+ return { ok: true };
319
+ }
320
+ let url;
321
+ try {
322
+ url = new URL(identifier);
323
+ } catch {
324
+ return {
325
+ ok: false,
326
+ reason: `resource identifier ${identifier} must be an absolute URI (RFC 8707 §2)`
327
+ };
328
+ }
329
+ if (url.hash) return {
330
+ ok: false,
331
+ reason: `resource identifier ${identifier} must not contain a URI fragment (RFC 8707 §2)`
332
+ };
333
+ return { ok: true };
334
+ }
335
+ /**
336
+ * Variant of {@link checkIdentifier} that throws `invalid_target` on failure.
337
+ * Used by admin CRUD endpoints where validation failure is a client error.
338
+ *
339
+ * @internal
340
+ */
341
+ async function assertIdentifierValid(opts, identifier) {
342
+ const result = await checkIdentifier(opts, identifier);
343
+ if (!result.ok) throw new APIError("BAD_REQUEST", {
344
+ error: "invalid_target",
345
+ error_description: result.reason
346
+ });
347
+ }
348
+ /**
349
+ * The OIDC userinfo endpoint's resource identifier. Always accepted as an
350
+ * implicit `aud` value when `openid` is in scope — not looked up against
351
+ * `oauthResource` rows.
352
+ */
353
+ const userInfoResource = (baseURL) => `${baseURL}/oauth2/userinfo`;
354
+ /**
355
+ * Re-parses a request's raw `application/x-www-form-urlencoded` body to
356
+ * recover every value of the repeated `resource` parameter (RFC 8707 §2).
357
+ *
358
+ * Workaround for better-call's form-body parser (better-call ≤1.3.5), which
359
+ * collapses repeated form keys with last-write-wins. A client sending
360
+ * `resource=https://a&resource=https://b` would otherwise arrive in the
361
+ * handler as `{ resource: "https://b" }`, silently narrowing the issued
362
+ * token's `aud` to a single resource.
363
+ *
364
+ * Returns the full ordered list when the body is form-encoded and contains
365
+ * any `resource` entries; `undefined` otherwise. Caller MUST enable
366
+ * `cloneRequest: true` on the endpoint — the body stream is read here a
367
+ * second time, and that only works when better-call cloned the request
368
+ * before its own parse.
369
+ *
370
+ * Note: the URL-encoded body path is the only one affected. The query-string
371
+ * path (e.g. `/oauth2/authorize?resource=…&resource=…`) is parsed by the
372
+ * router itself, which DOES array-promote duplicate keys correctly.
373
+ *
374
+ * @internal
375
+ */
376
+ async function extractRepeatedResourceFromForm(request) {
377
+ if (!(request.headers.get("content-type")?.toLowerCase() ?? "").includes("application/x-www-form-urlencoded")) return;
378
+ let text;
379
+ try {
380
+ text = await request.clone().text();
381
+ } catch {
382
+ return;
383
+ }
384
+ if (!text) return void 0;
385
+ const values = new URLSearchParams(text).getAll("resource").filter((value) => value.length > 0);
386
+ return values.length > 0 ? values : void 0;
387
+ }
388
+ /**
389
+ * Normalizes a `resource` parameter into an array. Accepts the RFC 8707
390
+ * forms: a single string, a string[], or undefined.
391
+ *
392
+ * @internal
393
+ */
394
+ function normalizeResourceParam(resource) {
395
+ if (resource === void 0 || resource === null) return void 0;
396
+ if (typeof resource === "string") return [resource];
397
+ if (Array.isArray(resource)) {
398
+ const result = resource.filter((r) => typeof r === "string" && r.length > 0);
399
+ return result.length > 0 ? result : void 0;
400
+ }
401
+ }
402
+ /**
403
+ * Validates a JWT `aud` claim against the OAuth resource model.
404
+ *
405
+ * Every non-implicit audience value must resolve to an `oauthResource` row.
406
+ * Disabled rows still pass because disabling blocks new issuance; deleting the
407
+ * row is the operation that invalidates already-issued tokens.
408
+ *
409
+ * @internal
410
+ */
411
+ async function isAudienceClaimAllowed(ctx, opts, audienceClaim, implicitAudiences = []) {
412
+ if (audienceClaim === void 0) return true;
413
+ const audienceValues = Array.isArray(audienceClaim) ? audienceClaim : [audienceClaim];
414
+ if (audienceValues.length > MAX_AUD_VALUES) return false;
415
+ const implicitAudienceSet = new Set(implicitAudiences);
416
+ const resourcesToLookup = /* @__PURE__ */ new Set();
417
+ for (const audienceValue of audienceValues) {
418
+ if (implicitAudienceSet.has(audienceValue)) continue;
419
+ resourcesToLookup.add(audienceValue);
420
+ }
421
+ if (resourcesToLookup.size === 0) return true;
422
+ return (await Promise.all(Array.from(resourcesToLookup, (resource) => getResource(ctx, opts, resource)))).every(Boolean);
423
+ }
424
+ /**
425
+ * Resolves the resource policy for a request.
426
+ *
427
+ * Validation steps (RFC 8707 §3 + per-resource policy):
428
+ *
429
+ * 1. Resolve `resource` parameter to a list of identifiers.
430
+ * 2. For each identifier, look up the {@link OAuthResource} row.
431
+ * - Missing → `invalid_target`.
432
+ * - `disabled` → `invalid_target` (no new issuance).
433
+ * 3. If {@link OAuthOptions.enforcePerClientResources} resolves to true,
434
+ * confirm the client is linked to every requested resource via
435
+ * `oauthClientResource`. Unlinked → `invalid_target`.
436
+ * 4. Intersect `requestedScopes` with each resource's `allowedScopes`.
437
+ * Empty intersection → `invalid_scope`.
438
+ * 5. Pick the minimum `accessTokenTtl` across requested resources.
439
+ * 6. Pick signing config — only honored for single-resource requests
440
+ * (a JWT can only have one signature).
441
+ * 7. Merge per-resource `customClaims` (returned raw; reserved-claim
442
+ * stripping is owned by `resolveAccessTokenClaims`).
443
+ *
444
+ * When no `resource` param is present, returns an empty policy: no `aud`
445
+ * claim and no resource-specific overrides.
446
+ *
447
+ * @internal
448
+ */
449
+ async function resolveResourcePolicy(ctx, opts, params) {
450
+ const requestedResources = normalizeResourceParam(params.resource);
451
+ const includesOpenid = params.requestedScopes.includes("openid");
452
+ const userInfoResourceIdentifier = userInfoResource(ctx.context.baseURL ?? "");
453
+ if (!requestedResources) return {
454
+ audienceClaim: void 0,
455
+ accessTokenTtl: null,
456
+ refreshTokenTtl: null,
457
+ signingAlgorithm: null,
458
+ signingKeyId: null,
459
+ rawCustomClaims: {},
460
+ dpopBoundAccessTokensRequired: false,
461
+ effectiveScopes: [...params.requestedScopes]
462
+ };
463
+ const uniqueRequestedResources = [...new Set(requestedResources)];
464
+ const resolved = [];
465
+ for (const identifier of uniqueRequestedResources) {
466
+ if (identifier === userInfoResourceIdentifier) continue;
467
+ const row = await getResource(ctx, opts, identifier);
468
+ if (row) {
469
+ if (row.disabled) throw new APIError("BAD_REQUEST", {
470
+ error: "invalid_target",
471
+ error_description: `requested resource ${identifier} is disabled`
472
+ });
473
+ resolved.push(row);
474
+ continue;
475
+ }
476
+ throw new APIError("BAD_REQUEST", {
477
+ error: "invalid_target",
478
+ error_description: `requested resource ${identifier} is not configured`
479
+ });
480
+ }
481
+ const { value: enforcePerClient } = resolveEnforcePerClientResources(opts);
482
+ if (enforcePerClient && resolved.length > 0) await assertClientLinkedToResources(ctx, opts, params.clientId, resolved);
483
+ let effectiveScopes = [...params.requestedScopes];
484
+ for (const row of resolved) {
485
+ if (row.allowedScopes === null || row.allowedScopes === void 0) continue;
486
+ const allowed = new Set(row.allowedScopes);
487
+ const intersection = effectiveScopes.filter((s) => allowed.has(s));
488
+ if (intersection.length === 0) throw new APIError("BAD_REQUEST", {
489
+ error: "invalid_scope",
490
+ error_description: `none of the requested scopes are allowed for resource ${row.identifier}`
491
+ });
492
+ effectiveScopes = intersection;
493
+ }
494
+ let accessTokenTtl = null;
495
+ let refreshTokenTtl = null;
496
+ for (const row of resolved) {
497
+ if (row.accessTokenTtl != null) accessTokenTtl = accessTokenTtl === null ? row.accessTokenTtl : Math.min(accessTokenTtl, row.accessTokenTtl);
498
+ if (row.refreshTokenTtl != null) refreshTokenTtl = refreshTokenTtl === null ? row.refreshTokenTtl : Math.min(refreshTokenTtl, row.refreshTokenTtl);
499
+ }
500
+ const uniqueSigningAlgs = /* @__PURE__ */ new Set();
501
+ const uniqueSigningKids = /* @__PURE__ */ new Set();
502
+ for (const row of resolved) {
503
+ if (row.signingAlgorithm) uniqueSigningAlgs.add(row.signingAlgorithm);
504
+ if (row.signingKeyId) uniqueSigningKids.add(row.signingKeyId);
505
+ }
506
+ if (uniqueSigningAlgs.size > 1) throw new APIError("BAD_REQUEST", {
507
+ error: "invalid_request",
508
+ error_description: "multi-resource request has conflicting signingAlgorithm pins; a single JWS signature cannot satisfy multiple algorithms"
509
+ });
510
+ if (uniqueSigningKids.size > 1) throw new APIError("BAD_REQUEST", {
511
+ error: "invalid_request",
512
+ error_description: "multi-resource request has conflicting signingKeyId pins; a single JWS signature cannot satisfy multiple key ids"
513
+ });
514
+ const signingAlgorithm = uniqueSigningAlgs.values().next().value ?? null;
515
+ const signingKeyId = uniqueSigningKids.values().next().value ?? null;
516
+ const mergedClaims = {};
517
+ for (const row of resolved) if (row.customClaims && typeof row.customClaims === "object") Object.assign(mergedClaims, row.customClaims);
518
+ const dpopBoundAccessTokensRequired = resolved.some((row) => row.dpopBoundAccessTokensRequired === true);
519
+ const audienceIdentifiers = includesOpenid ? [...uniqueRequestedResources, userInfoResourceIdentifier] : uniqueRequestedResources;
520
+ const audClaim = [...new Set(audienceIdentifiers)];
521
+ return {
522
+ audienceClaim: audClaim.length === 1 ? audClaim[0] : audClaim,
523
+ accessTokenTtl,
524
+ refreshTokenTtl,
525
+ signingAlgorithm,
526
+ signingKeyId,
527
+ rawCustomClaims: mergedClaims,
528
+ dpopBoundAccessTokensRequired,
529
+ effectiveScopes
530
+ };
531
+ }
532
+ /**
533
+ * Throws `invalid_target` if the client isn't linked to every resource via
534
+ * the `oauthClientResource` join table. Issues one `findMany` per call.
535
+ *
536
+ * @internal
537
+ */
538
+ async function assertClientLinkedToResources(ctx, opts, clientId, resources) {
539
+ if (resources.length === 0) return;
540
+ const modelName = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
541
+ const links = await ctx.context.adapter.findMany({
542
+ model: modelName,
543
+ where: [{
544
+ field: "clientId",
545
+ value: clientId
546
+ }]
547
+ });
548
+ const linkedSet = new Set(links?.map((l) => l.resourceId) ?? []);
549
+ const unlinked = resources.filter((resource) => !linkedSet.has(resource.identifier));
550
+ if (unlinked.length > 0) throw new APIError("BAD_REQUEST", {
551
+ error: "invalid_target",
552
+ error_description: `client ${clientId} is not linked to resource(s) ${unlinked.map((resource) => resource.identifier).join(", ")}`
553
+ });
554
+ }
555
+ /**
556
+ * Returns `true` when `clientId` is linked, via the `oauthClientResource` join
557
+ * table, to at least one of `resourceIdentifiers`. Authorizes cross-client token
558
+ * introspection (RFC 7662 §2.1/§4): a resource server may introspect a token
559
+ * whose audience it serves, even when a different client issued the token.
560
+ *
561
+ * @internal
562
+ */
563
+ async function isClientLinkedToAnyResource(ctx, opts, clientId, resourceIdentifiers) {
564
+ if (resourceIdentifiers.length === 0) return false;
565
+ const modelName = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
566
+ return ((await ctx.context.adapter.findMany({
567
+ model: modelName,
568
+ where: [{
569
+ field: "clientId",
570
+ value: clientId
571
+ }, {
572
+ field: "resourceId",
573
+ operator: "in",
574
+ value: resourceIdentifiers
575
+ }],
576
+ limit: 1
577
+ }))?.length ?? 0) > 0;
578
+ }
579
+ /**
580
+ * Merges `customClaims` from the existing rows of `resourceIdentifiers` in
581
+ * order (later resources win on collision). Missing rows are skipped; disabled
582
+ * rows are included, since an already-issued token keeps its claims until it
583
+ * expires. Returned raw; `resolveAccessTokenClaims` strips reserved names.
584
+ *
585
+ * Unlike {@link resolveResourcePolicy}, this applies none of the new-issuance
586
+ * gates (disabled, scope allowlist, client linkage), so it is the right way to
587
+ * re-derive claims for an already-issued token at introspection.
588
+ *
589
+ * @internal
590
+ */
591
+ async function getResourceCustomClaims(ctx, opts, resourceIdentifiers) {
592
+ if (resourceIdentifiers.length === 0) return {};
593
+ const rows = await ctx.context.adapter.findMany({
594
+ model: opts.schema?.oauthResource?.modelName ?? "oauthResource",
595
+ where: [{
596
+ field: "identifier",
597
+ operator: "in",
598
+ value: resourceIdentifiers
599
+ }]
600
+ });
601
+ const rowByIdentifier = new Map(rows.map((row) => [row.identifier, row]));
602
+ const merged = {};
603
+ for (const identifier of resourceIdentifiers) {
604
+ const row = rowByIdentifier.get(identifier);
605
+ if (row?.customClaims && typeof row.customClaims === "object") Object.assign(merged, row.customClaims);
606
+ }
607
+ return merged;
608
+ }
609
+ /**
610
+ * Resolves the effective {@link OAuthOptions.enforcePerClientResources} value.
611
+ *
612
+ * - Explicit `true | false` always wins (`source: "explicit"`).
613
+ * - Otherwise resolve to `true` (`source: "default"`). RFC 8707 §3 per-client
614
+ * validation is the secure default.
615
+ *
616
+ * Deterministic and pure — safe to call on every validation pass.
617
+ */
618
+ function resolveEnforcePerClientResources(opts) {
619
+ if (opts.enforcePerClientResources !== void 0) return {
620
+ value: opts.enforcePerClientResources,
621
+ source: "explicit"
622
+ };
623
+ return {
624
+ value: true,
625
+ source: "default"
626
+ };
627
+ }
628
+ /**
629
+ * In-process cache of {@link OAuthResource} rows, keyed by `identifier`.
630
+ *
631
+ * Opt-in via {@link OAuthOptions.cachedResources} (same membership-Set pattern
632
+ * as `cachedTrustedClients`). Resources outside the set are looked up from the
633
+ * DB on every request — the safe default for deployments that edit rows
634
+ * through external tooling.
635
+ *
636
+ * Module-scoped so admin CRUD handlers can invalidate from anywhere via
637
+ * {@link invalidateResourceCache}.
638
+ */
639
+ const resourceCache = /* @__PURE__ */ new Map();
640
+ /**
641
+ * Removes an entry from the resource cache. Called by admin CRUD handlers
642
+ * after every write. Pass no argument to clear the entire cache.
643
+ *
644
+ * @internal
645
+ */
646
+ function invalidateResourceCache(identifier) {
647
+ if (identifier === void 0) {
648
+ resourceCache.clear();
649
+ return;
650
+ }
651
+ resourceCache.delete(identifier);
652
+ }
653
+ /**
654
+ * Looks up an {@link OAuthResource} by `identifier`, consulting the in-process
655
+ * cache when the identifier is in {@link OAuthOptions.cachedResources}.
656
+ *
657
+ * Triggers the lazy seed via {@link seedResourcesOnce} on first call — this
658
+ * is the safety net for deployments where migrations run after plugin init
659
+ * (so seeding at init couldn't see the table yet). The seed is idempotent
660
+ * and coalesced, so the cost is paid only once per process.
661
+ *
662
+ * Returns a defensive copy so callers can't mutate cached state.
663
+ *
664
+ * @internal
665
+ */
666
+ async function getResource(ctx, opts, identifier) {
667
+ await seedResourcesOnce(ctx.context, opts);
668
+ if (opts.cachedResources?.has(identifier)) {
669
+ const cached = resourceCache.get(identifier);
670
+ if (cached) return Object.assign({}, cached);
671
+ }
672
+ const dbResource = await ctx.context.adapter.findOne({
673
+ model: opts.schema?.oauthResource?.modelName ?? "oauthResource",
674
+ where: [{
675
+ field: "identifier",
676
+ value: identifier
677
+ }]
678
+ });
679
+ if (dbResource && opts.cachedResources?.has(identifier)) resourceCache.set(identifier, Object.assign({}, dbResource));
680
+ return dbResource;
681
+ }
682
+ /**
683
+ * Default `name` value when an input doesn't provide one. Mirrors what most
684
+ * admin UIs would show.
685
+ */
686
+ function defaultName(input) {
687
+ return input.name ?? input.identifier;
688
+ }
689
+ /**
690
+ * Coerces seed-config `resources` entries — string or object form — into a
691
+ * normalized list of {@link OAuthResourceInput}.
692
+ *
693
+ * @internal
694
+ */
695
+ function collectResourceInputs(opts) {
696
+ const inputs = [];
697
+ if (opts.resources?.length) for (const entry of opts.resources) inputs.push(typeof entry === "string" ? { identifier: entry } : entry);
698
+ return inputs;
699
+ }
700
+ /**
701
+ * Builds the row payload sent to the adapter for a seed insert. All policy
702
+ * columns default to `null` (= inherit plugin default at issuance time)
703
+ * when the input doesn't specify a value.
704
+ */
705
+ function buildSeedRow(input, now) {
706
+ return {
707
+ identifier: input.identifier,
708
+ name: defaultName(input),
709
+ accessTokenTtl: input.accessTokenTtl ?? null,
710
+ refreshTokenTtl: input.refreshTokenTtl ?? null,
711
+ signingAlgorithm: input.signingAlgorithm ?? null,
712
+ signingKeyId: input.signingKeyId ?? null,
713
+ allowedScopes: input.allowedScopes ?? null,
714
+ customClaims: input.customClaims ?? null,
715
+ dpopBoundAccessTokensRequired: input.dpopBoundAccessTokensRequired ?? false,
716
+ disabled: input.disabled ?? false,
717
+ policyVersion: 1,
718
+ metadata: input.metadata ?? null,
719
+ createdAt: now,
720
+ updatedAt: now
721
+ };
722
+ }
723
+ /**
724
+ * Builds the partial update payload when re-seeding an existing row.
725
+ *
726
+ * - `overwrite` mode replaces every policy column with the input value
727
+ * (omitted fields fall back to `null` to clear stale state).
728
+ * - `merge` mode updates only fields present in the input — admin edits to
729
+ * other fields are preserved.
730
+ */
731
+ function buildSeedUpdate(input, mode, now) {
732
+ if (mode === "overwrite") {
733
+ const { identifier: _identifier, createdAt: _createdAt, ...rest } = buildSeedRow(input, now);
734
+ return rest;
735
+ }
736
+ const update = { updatedAt: now };
737
+ if (input.name !== void 0) update.name = input.name;
738
+ if (input.accessTokenTtl !== void 0) update.accessTokenTtl = input.accessTokenTtl;
739
+ if (input.refreshTokenTtl !== void 0) update.refreshTokenTtl = input.refreshTokenTtl;
740
+ if (input.signingAlgorithm !== void 0) update.signingAlgorithm = input.signingAlgorithm;
741
+ if (input.signingKeyId !== void 0) update.signingKeyId = input.signingKeyId;
742
+ if (input.allowedScopes !== void 0) update.allowedScopes = input.allowedScopes;
743
+ if (input.customClaims !== void 0) update.customClaims = input.customClaims;
744
+ if (input.dpopBoundAccessTokensRequired !== void 0) update.dpopBoundAccessTokensRequired = input.dpopBoundAccessTokensRequired;
745
+ if (input.disabled !== void 0) update.disabled = input.disabled;
746
+ if (input.metadata !== void 0) update.metadata = input.metadata;
747
+ return update;
748
+ }
749
+ /**
750
+ * Pattern matched against adapter errors to detect the "table not yet
751
+ * created" case — i.e. migrations haven't been run. When matched, the
752
+ * caller treats the seed as deferred (will retry on first resource access).
753
+ *
754
+ * Covers SQLite ("no such table"), Postgres ("relation X does not exist"),
755
+ * and MySQL ("Table X does not exist" / contracted form).
756
+ */
757
+ const MISSING_TABLE_PATTERN = /no such table|relation.*does not exist|table.*does(?: not|n[''']?t) exist/i;
758
+ /**
759
+ * Per-adapter state for the lazy-seed path. A module-level boolean would let
760
+ * one Better Auth instance suppress seeding for every later instance in the same
761
+ * process. Endpoint `AuthContext` objects are not guaranteed to be stable across
762
+ * requests, so keying by the adapter keeps one seed state per backing store.
763
+ */
764
+ let seedStates = /* @__PURE__ */ new WeakMap();
765
+ function getSeedState(ctx) {
766
+ const key = ctx.adapter;
767
+ const existing = seedStates.get(key);
768
+ if (existing) return existing;
769
+ const created = {
770
+ completed: false,
771
+ promise: null
772
+ };
773
+ seedStates.set(key, created);
774
+ return created;
775
+ }
776
+ /**
777
+ * Seeds `oauthResource` rows from plugin config.
778
+ *
779
+ * Behavior is controlled by {@link OAuthOptions.resourceSeedMode}:
780
+ *
781
+ * - `"insertOnly"` (default, safe): inserts rows whose `identifier` is not
782
+ * already present. Existing rows are untouched — admin edits via CRUD
783
+ * are never reverted on restart.
784
+ * - `"merge"`: inserts missing rows; updates only specified fields for
785
+ * existing rows. Useful when config holds the "preferred defaults" but
786
+ * admins customize per-row.
787
+ * - `"overwrite"`: inserts missing rows; replaces every policy column on
788
+ * existing rows with the config value (omitted fields → null). Use only
789
+ * when config is the single source of truth.
790
+ *
791
+ * Race-safety: the `identifier` column carries a UNIQUE constraint, so two
792
+ * processes booting simultaneously can each attempt the insert — one wins,
793
+ * the other catches the constraint error and treats it as a no-op.
794
+ *
795
+ * Migration ordering: at plugin `init` time, tables may not exist yet
796
+ * (Better Auth's test harness, and many deployment setups, run migrations
797
+ * after auth construction). Seeding tolerates "no such table" errors and
798
+ * defers — the lazy {@link seedResourcesOnce} path picks up the work on
799
+ * the first resource access.
800
+ *
801
+ * Idempotent: safe to call multiple times.
802
+ *
803
+ * @internal
804
+ */
805
+ async function seedResources(ctx, opts) {
806
+ const inputs = collectResourceInputs(opts);
807
+ if (inputs.length === 0) return;
808
+ const mode = opts.resourceSeedMode ?? "insertOnly";
809
+ const modelName = opts.schema?.oauthResource?.modelName ?? "oauthResource";
810
+ for (const rawInput of inputs) {
811
+ const check = await checkIdentifier(opts, rawInput.identifier);
812
+ if (!check.ok) {
813
+ logger.warn(`oauth-provider: skipping resource seed for ${rawInput.identifier} — ${check.reason}`);
814
+ continue;
815
+ }
816
+ let input = rawInput;
817
+ if (input.signingAlgorithm != null && !JWS_ALGORITHM_SET.has(input.signingAlgorithm)) {
818
+ logger.warn(`oauth-provider: dropping unsupported signingAlgorithm "${input.signingAlgorithm}" for resource ${input.identifier} — must be one of ${JWS_ALGORITHMS.join(", ")}. Continuing without an algorithm override.`);
819
+ const { signingAlgorithm: _, ...rest } = input;
820
+ input = rest;
821
+ }
822
+ let existing;
823
+ try {
824
+ existing = await ctx.adapter.findOne({
825
+ model: modelName,
826
+ where: [{
827
+ field: "identifier",
828
+ value: input.identifier
829
+ }]
830
+ });
831
+ } catch (err) {
832
+ const message = err instanceof Error ? err.message : String(err);
833
+ if (MISSING_TABLE_PATTERN.test(message)) {
834
+ logger.debug("oauth-provider: oauthResource table not yet created; deferring resource seed to first access.");
835
+ return;
836
+ }
837
+ throw err;
838
+ }
839
+ const now = /* @__PURE__ */ new Date();
840
+ if (!existing) {
841
+ try {
842
+ await ctx.adapter.create({
843
+ model: modelName,
844
+ data: buildSeedRow(input, now)
845
+ });
846
+ } catch (err) {
847
+ const message = err instanceof Error ? err.message : String(err);
848
+ if (/unique|duplicate|UNIQUE/i.test(message)) {
849
+ logger.debug(`oauth-provider: resource ${input.identifier} already inserted by a concurrent process — skipping.`);
850
+ continue;
851
+ }
852
+ if (MISSING_TABLE_PATTERN.test(message)) {
853
+ logger.debug("oauth-provider: oauthResource table not yet created; deferring resource seed to first access.");
854
+ return;
855
+ }
856
+ throw err;
857
+ }
858
+ continue;
859
+ }
860
+ if (mode === "insertOnly") continue;
861
+ await ctx.adapter.update({
862
+ model: modelName,
863
+ where: [{
864
+ field: "identifier",
865
+ value: input.identifier
866
+ }],
867
+ update: buildSeedUpdate(input, mode, now)
868
+ });
869
+ }
870
+ }
871
+ /**
872
+ * Idempotent, coalesced wrapper around {@link seedResources} for the
873
+ * lazy-seed path. Safe to call from every resource lookup — the first call
874
+ * runs the seed, concurrent calls await the same promise, and subsequent
875
+ * calls become no-ops.
876
+ *
877
+ * The endpoint context shape (`{ context: AuthContext }`) is what
878
+ * `getResource` receives, so this is a convenience overload that unwraps
879
+ * for callers.
880
+ *
881
+ * @internal
882
+ */
883
+ async function seedResourcesOnce(ctx, opts) {
884
+ const state = getSeedState(ctx);
885
+ if (state.completed === true) return;
886
+ if (state.promise !== null) return state.promise;
887
+ state.promise = seedResources(ctx, opts).then(() => {
888
+ state.completed = true;
889
+ }).catch((err) => {
890
+ state.promise = null;
891
+ throw err;
892
+ });
893
+ return state.promise;
894
+ }
895
+ /**
896
+ * Logs the resolved value of {@link OAuthOptions.enforcePerClientResources}
897
+ * so deployment admins see which default applied at init. Separated from
898
+ * {@link resolveEnforcePerClientResources} so the latter stays pure and
899
+ * cheap to call from validation flow.
900
+ *
901
+ * @internal
902
+ */
903
+ function logEnforcePerClientResourcesResolution(opts) {
904
+ const resolved = resolveEnforcePerClientResources(opts);
905
+ logger.info(`oauth-provider: enforcePerClientResources resolved to ${resolved.value} (${resolved.source})`);
906
+ }
907
+ //#endregion
908
+ //#region src/dpop.ts
909
+ function getDpopProofJwt(ctx) {
910
+ return ctx.headers?.get("dpop") ?? void 0;
911
+ }
912
+ function getEndpointUrl(ctx, path) {
913
+ return ctx.request?.url ?? `${ctx.context.baseURL}${path}`;
914
+ }
915
+ //#endregion
916
+ //#region src/oauthClient/client-credentials.ts
917
+ const USER_DELEGATED_SCOPES = new Set([
918
+ "openid",
919
+ "profile",
920
+ "email",
921
+ "offline_access"
922
+ ]);
923
+ function isUserDelegatedScope(scope) {
924
+ return USER_DELEGATED_SCOPES.has(scope);
925
+ }
926
+ function normalizeClientCredentialsScopes(scopes) {
927
+ return [...new Set(scopes.map((scope) => scope.trim()).filter(Boolean))];
928
+ }
929
+ function validateClientCredentialsScopes(scopes, grantTypes, tokenEndpointAuthMethod, opts) {
930
+ if (scopes.length === 0) return;
931
+ if (!grantTypes.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
932
+ error: "invalid_client_metadata",
933
+ error_description: "client_credentials_scopes requires the client_credentials grant"
934
+ });
935
+ if (tokenEndpointAuthMethod === "none") throw new APIError("BAD_REQUEST", {
936
+ error: "invalid_client_metadata",
937
+ error_description: "public clients cannot be assigned client_credentials scopes"
938
+ });
939
+ const providerScopes = new Set(opts.scopes ?? []);
940
+ const invalidScopes = scopes.filter((scope) => !providerScopes.has(scope) || isUserDelegatedScope(scope));
941
+ if (invalidScopes.length > 0) throw new APIError("BAD_REQUEST", {
942
+ error: "invalid_scope",
943
+ error_description: `The following client_credentials scopes are invalid: ${invalidScopes.join(", ")}`
944
+ });
945
+ }
946
+ //#endregion
947
+ //#region src/types/zod.ts
948
+ const DANGEROUS_SCHEMES = [
949
+ "javascript:",
950
+ "data:",
951
+ "vbscript:"
952
+ ];
953
+ /**
954
+ * Validates an RFC 8707 resource indicator. The value must be an absolute URI
955
+ * with no fragment (RFC 8707 §2). Unlike a redirect URI it is not restricted to
956
+ * HTTPS, because a resource server identifier may use any absolute URI scheme;
957
+ * configured OAuth resources are the authoritative control over
958
+ * which resources a token may target.
959
+ */
960
+ const ResourceUriSchema = z.string().superRefine((val, ctx) => {
961
+ if (!URL.canParse(val)) {
962
+ ctx.addIssue({
963
+ code: "custom",
964
+ message: "resource must be an absolute URI",
965
+ fatal: true
966
+ });
967
+ return z.NEVER;
968
+ }
969
+ if (val.includes("#")) {
970
+ ctx.addIssue({
971
+ code: "custom",
972
+ message: "resource must not contain a fragment"
973
+ });
974
+ return;
975
+ }
976
+ if (DANGEROUS_SCHEMES.includes(new URL(val).protocol)) ctx.addIssue({
977
+ code: "custom",
978
+ message: "resource cannot use javascript:, data:, or vbscript: scheme"
979
+ });
980
+ });
981
+ const authorizationPromptTokenSchema = z.enum([
982
+ "none",
983
+ "consent",
984
+ "login",
985
+ "create",
986
+ "select_account"
987
+ ]);
988
+ const authorizationPromptSchema = z.string().superRefine((value, ctx) => {
989
+ const promptTokens = value.split(" ").map((token) => token.trim()).filter(Boolean);
990
+ const promptSet = /* @__PURE__ */ new Set();
991
+ if (!promptTokens.length) {
992
+ ctx.addIssue({
993
+ code: "custom",
994
+ message: "prompt must include at least one value"
995
+ });
996
+ return;
997
+ }
998
+ for (const token of promptTokens) {
999
+ const result = authorizationPromptTokenSchema.safeParse(token);
1000
+ if (!result.success) {
1001
+ ctx.addIssue({
1002
+ code: "custom",
1003
+ message: `unsupported prompt value: ${token}`
1004
+ });
1005
+ continue;
1006
+ }
1007
+ promptSet.add(result.data);
1008
+ }
1009
+ if (promptSet.has("none") && promptSet.size > 1) ctx.addIssue({
1010
+ code: "custom",
1011
+ message: "prompt=none cannot be combined with other prompt values"
1012
+ });
1013
+ });
1014
+ /** Canonical RFC 7517 JWK Set metadata for an OAuth client. */
1015
+ const clientJwksSchema = z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) });
1016
+ const maxAgeSchema = z.union([z.number(), z.string().trim().min(1)]).transform((value, ctx) => {
1017
+ const maxAge = typeof value === "number" ? value : Number(value);
1018
+ if (!Number.isInteger(maxAge) || maxAge < 0) {
1019
+ ctx.addIssue({
1020
+ code: "custom",
1021
+ message: "max_age must be a non-negative integer"
1022
+ });
1023
+ return z.NEVER;
1024
+ }
1025
+ return maxAge;
1026
+ });
1027
+ const dpopJktSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/, "dpop_jkt must be a base64url-encoded SHA-256 JWK thumbprint");
1028
+ /**
1029
+ * Runtime schema for OAuthAuthorizationQuery.
1030
+ * Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
1031
+ */
1032
+ const authorizationQuerySchema = z.object({
1033
+ response_type: z.string().pipe(z.enum(["code"])).optional(),
1034
+ request: z.string().optional(),
1035
+ request_uri: z.string().optional(),
1036
+ redirect_uri: SafeUrlSchema.optional(),
1037
+ scope: z.string().optional(),
1038
+ state: z.string().optional(),
1039
+ client_id: z.string().min(1, "client_id is required"),
1040
+ prompt: authorizationPromptSchema.optional(),
1041
+ display: z.string().optional(),
1042
+ ui_locales: z.string().optional(),
1043
+ max_age: maxAgeSchema.optional(),
1044
+ acr_values: z.string().optional(),
1045
+ login_hint: z.string().optional(),
1046
+ id_token_hint: z.string().optional(),
1047
+ code_challenge: z.string().optional(),
1048
+ code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
1049
+ nonce: z.string().optional(),
1050
+ claims: claimsRequestInputSchema.optional(),
1051
+ dpop_jkt: dpopJktSchema.optional(),
1052
+ resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional()
1053
+ }).passthrough();
1054
+ const storedAuthorizationQuerySchema = authorizationQuerySchema.extend({
1055
+ redirect_uri: SafeUrlSchema.optional(),
1056
+ claims: claimsRequestParameterSchema.optional()
1057
+ });
1058
+ /**
1059
+ * Runtime schema for the authorization code verification value.
1060
+ * Validates structure on deserialization from the JSON blob stored in the DB.
1061
+ * Uses passthrough so future fields (e.g. from authorization challenge) don't break parsing.
1062
+ */
1063
+ const verificationValueSchema = z.object({
1064
+ type: z.literal("authorization_code"),
1065
+ query: storedAuthorizationQuerySchema,
1066
+ sessionId: z.string(),
1067
+ userId: z.string(),
1068
+ referenceId: z.string().optional(),
1069
+ authTime: z.number().optional(),
1070
+ resource: z.array(z.string()).optional()
1071
+ }).passthrough();
1072
+ /**
1073
+ * Request body accepted at `POST /oauth2/register` (RFC 7591 §2 client
1074
+ * metadata). This is the single source of truth for the registration contract:
1075
+ * the endpoint validates against it and {@link ClientRegistrationRequest} is
1076
+ * inferred from it, so the type a `validateInitialAccessToken` callback receives
1077
+ * always matches what is actually validated. `grant_types` and
1078
+ * `token_endpoint_auth_method` are open strings because extensions can register
1079
+ * custom values. Server-assigned fields (`client_id`, `client_secret`, the
1080
+ * issued/expiry timestamps) and internal state (`disabled`, `reference_id`) are
1081
+ * never part of a registration request.
1082
+ *
1083
+ * @see https://datatracker.ietf.org/doc/html/rfc7591#section-2
1084
+ */
1085
+ const clientRegistrationRequestSchema = z.object({
1086
+ redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
1087
+ scope: z.string().optional(),
1088
+ client_name: z.string().refine((value) => value.trim().length > 0, "client_name cannot be empty").optional(),
1089
+ client_uri: z.string().optional(),
1090
+ logo_uri: z.string().optional(),
1091
+ contacts: z.array(z.string().min(1)).min(1).optional(),
1092
+ tos_uri: z.string().optional(),
1093
+ policy_uri: z.string().optional(),
1094
+ software_id: z.string().optional(),
1095
+ software_version: z.string().optional(),
1096
+ software_statement: z.string().optional(),
1097
+ post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
1098
+ backchannel_logout_uri: SafeUrlSchema.optional(),
1099
+ backchannel_logout_session_required: z.boolean().optional(),
1100
+ token_endpoint_auth_method: z.string().trim().min(1).optional(),
1101
+ jwks: clientJwksSchema.optional(),
1102
+ jwks_uri: z.string().optional(),
1103
+ grant_types: z.array(z.string().trim().min(1)).min(1).optional(),
1104
+ response_types: z.array(z.enum(["code"])).optional(),
1105
+ application_type: z.enum(["web", "native"]).optional(),
1106
+ subject_type: z.enum(["public", "pairwise"]).optional(),
1107
+ dpop_bound_access_tokens: z.boolean().optional(),
1108
+ resources: z.array(ResourceUriSchema).optional(),
1109
+ skip_consent: z.never({ error: "skip_consent cannot be set during dynamic client registration" }).optional()
1110
+ });
1111
+ /**
1112
+ * Complete OAuth client metadata document schema shared by registration and
1113
+ * verified client-discovery integrations.
1114
+ *
1115
+ * Unlike {@link clientRegistrationRequestSchema}, a metadata document carries
1116
+ * its own identifier and may contain the server-assigned secret fields that a
1117
+ * discovery profile must explicitly prohibit. Keeping those fields in the
1118
+ * parsed shape lets profile validation distinguish a forbidden value from an
1119
+ * unknown extension without unsafe casts or ad-hoc allowlists.
1120
+ */
1121
+ const oauthClientMetadataSchema = clientRegistrationRequestSchema.omit({ skip_consent: true }).extend({
1122
+ client_id: z.string().refine((value) => value.trim().length > 0, "client_id cannot be empty"),
1123
+ client_secret: z.string().optional(),
1124
+ client_secret_expires_at: z.number().int().nonnegative().optional(),
1125
+ client_id_issued_at: z.number().int().nonnegative().optional()
1126
+ }).loose();
1127
+ //#endregion
1128
+ //#region src/userinfo.ts
1129
+ /**
1130
+ * Builds the standard OIDC claims (OIDC Core §5.1) for the UserInfo response.
1131
+ *
1132
+ * A claim is included when its backing scope was granted, or when it was named
1133
+ * individually through the `claims.userinfo` request parameter (§5.4, §5.5).
1134
+ * `sub` is always present. Values come from the one claim registry, so the
1135
+ * advertisement, the scope mapping, and the resolution cannot drift apart.
1136
+ *
1137
+ * @see https://openid.net/specs/openid-connect-core-1_0.html#NormalClaims
1138
+ */
1139
+ function userNormalClaims(user, scopes, requestedClaims = []) {
1140
+ const requested = new Set(requestedClaims);
1141
+ const claims = { sub: user.id ?? void 0 };
1142
+ for (const [name, definition] of Object.entries(STANDARD_CLAIMS)) if (scopes.includes(definition.scope) || requested.has(name)) claims[name] = definition.resolve(user);
1143
+ return claims;
1144
+ }
1145
+ /**
1146
+ * Returns the defined-valued entries of `claims`, dropping any key already
1147
+ * present in `base` when given.
1148
+ *
1149
+ * This is the two-tier claim authority shared by the /userinfo response and the
1150
+ * ID token:
1151
+ * - Called WITH `base` (the provider's own claims): the additive rule for
1152
+ * third-party extension claims. A contributor may add new keys but never
1153
+ * replace a claim the provider already owns.
1154
+ * - Called WITHOUT `base`: the deliberate first-party override path for the
1155
+ * operator's own `customUserInfoClaims` / `customIdTokenClaims`, which is
1156
+ * trusted to override identity claims (for example a formatted `name`). The
1157
+ * caller re-pins `sub` afterwards, so subject integrity holds either way
1158
+ * (OIDC Core §5.3.2: UserInfo `sub` MUST match the ID Token `sub`).
1159
+ */
1160
+ function pickClaims(claims, base) {
1161
+ const next = {};
1162
+ for (const [key, value] of Object.entries(claims ?? {})) {
1163
+ if (value === void 0) continue;
1164
+ if (base && key in base) continue;
1165
+ next[key] = value;
1166
+ }
1167
+ return next;
1168
+ }
1169
+ function getUserInfoAccessToken(ctx) {
1170
+ const authorization = ctx.headers?.get("authorization");
1171
+ const headerAccessTokenAuthorization = parseAccessTokenAuthorization(authorization);
1172
+ const bodyToken = ctx.request?.method === "POST" ? ctx.body?.access_token ?? void 0 : void 0;
1173
+ if (headerAccessTokenAuthorization && bodyToken) throw new APIError("BAD_REQUEST", {
1174
+ error_description: "Multiple access token transport methods are not allowed",
1175
+ error: "invalid_request"
1176
+ });
1177
+ const accessTokenAuthorization = headerAccessTokenAuthorization ?? (bodyToken ? {
1178
+ scheme: "Bearer",
1179
+ token: bodyToken
1180
+ } : void 0);
1181
+ return {
1182
+ authorization: accessTokenAuthorization,
1183
+ token: accessTokenAuthorization?.token
1184
+ };
1185
+ }
1186
+ /**
1187
+ * Handles the /oauth2/userinfo endpoint
1188
+ */
1189
+ async function userInfoEndpoint(ctx, opts) {
1190
+ const { authorization: accessTokenAuthorization } = getUserInfoAccessToken(ctx);
1191
+ if (!accessTokenAuthorization?.token) throw new APIError("UNAUTHORIZED", {
1192
+ error_description: "access token not found",
1193
+ error: "invalid_request"
1194
+ });
1195
+ const { payload: jwt, requestedUserInfoClaims: requestedClaims } = await requireActiveAccessTokenWithClaims(ctx, opts, accessTokenAuthorization.token);
1196
+ if (getDpopJktFromPayload(jwt) && !ctx.request) throw new APIError("UNAUTHORIZED", {
1197
+ error_description: "DPoP-bound access token requires an HTTP request context",
1198
+ error: "invalid_token"
1199
+ });
1200
+ try {
1201
+ await enforceDpopBinding({
1202
+ payload: jwt,
1203
+ authorization: accessTokenAuthorization,
1204
+ proofJwt: getDpopProofJwt(ctx),
1205
+ method: ctx.request?.method ?? "GET",
1206
+ url: getEndpointUrl(ctx, "/oauth2/userinfo"),
1207
+ proofMaxAgeSeconds: opts.dpop?.proofMaxAgeSeconds,
1208
+ signingAlgorithms: opts.dpop?.signingAlgorithms,
1209
+ replayStore: createDpopReplayStore(ctx.context.internalAdapter)
1210
+ });
1211
+ } catch (error) {
1212
+ if (isDpopBindingError(error)) throw new APIError("UNAUTHORIZED", {
1213
+ error_description: error.message,
1214
+ error: error.code
1215
+ });
1216
+ throw error;
1217
+ }
1218
+ const scopes = jwt.scope?.split(" ");
1219
+ if (!scopes?.includes("openid")) throw new APIError("BAD_REQUEST", {
1220
+ error_description: "Missing required scope",
1221
+ error: "invalid_scope"
1222
+ });
1223
+ if (!jwt.sub) throw new APIError("BAD_REQUEST", {
1224
+ error_description: "user not found",
1225
+ error: "invalid_request"
1226
+ });
1227
+ const user = await ctx.context.internalAdapter.findUserById(jwt.sub);
1228
+ if (!user) throw new APIError("BAD_REQUEST", {
1229
+ error_description: "user not found",
1230
+ error: "invalid_request"
1231
+ });
1232
+ const baseUserClaims = userNormalClaims(user, scopes ?? [], requestedClaims);
1233
+ const clientId = jwt.client_id ?? jwt.azp;
1234
+ const client = clientId && (opts.pairwiseSecret || hasUserInfoClaimExtension(opts)) ? await getClient(ctx, opts, clientId) : void 0;
1235
+ if (opts.pairwiseSecret && client) baseUserClaims.sub = await resolveSubjectIdentifier(user.id, client, opts);
1236
+ const extensionUserClaims = scopes?.length ? await collectExtensionUserInfoClaims(opts, {
1237
+ ctx,
1238
+ opts,
1239
+ user,
1240
+ scopes,
1241
+ jwt,
1242
+ client: client ?? void 0,
1243
+ requestedClaims
1244
+ }) : {};
1245
+ const additionalInfoUserClaims = opts.customUserInfoClaims && scopes?.length ? await opts.customUserInfoClaims({
1246
+ user,
1247
+ scopes,
1248
+ jwt,
1249
+ requestedClaims
1250
+ }) : {};
1251
+ return {
1252
+ ...baseUserClaims,
1253
+ ...pickClaims(extensionUserClaims, baseUserClaims),
1254
+ ...pickClaims(additionalInfoUserClaims),
1255
+ sub: baseUserClaims.sub
1256
+ };
1257
+ }
1258
+ //#endregion
1259
+ //#region src/token.ts
1260
+ const ID_TOKEN_SCOPE_CLAIM_GUARDS = Object.fromEntries(STANDARD_CLAIM_NAMES.map((name) => [name, void 0]));
1261
+ /**
1262
+ * Token presentation scheme implied by a confirmation: a DPoP key thumbprint
1263
+ * (`jkt`) yields `"DPoP"`; any other confirmation (including mTLS `x5t#S256`)
1264
+ * keeps `"Bearer"`, since that constraint lives at the TLS layer.
1265
+ */
1266
+ function confirmationTokenType(confirmation) {
1267
+ return getConfirmationJkt(confirmation) ? "DPoP" : "Bearer";
1268
+ }
1269
+ const JWT_ACCESS_TOKEN_TYPE = "at+jwt";
1270
+ /**
1271
+ * Handles the /oauth2/token endpoint by delegating
1272
+ * the grant types
1273
+ */
1274
+ async function tokenEndpoint(ctx, opts) {
1275
+ const grantType = ctx.body.grant_type;
1276
+ if (!getSupportedGrantTypes(opts).includes(grantType)) throw new APIError("BAD_REQUEST", {
1277
+ error_description: `unsupported grant_type ${grantType}`,
1278
+ error: "unsupported_grant_type"
1279
+ });
1280
+ switch (grantType) {
1281
+ case "authorization_code": return handleAuthorizationCodeGrant(ctx, opts);
1282
+ case "client_credentials": return handleClientCredentialsGrant(ctx, opts);
1283
+ case "refresh_token": return handleRefreshTokenGrant(ctx, opts);
1284
+ default: {
1285
+ const handler = getExtensionGrantHandler(opts, grantType);
1286
+ if (handler) return handler({
1287
+ ctx,
1288
+ opts,
1289
+ grantType,
1290
+ provider: getOAuthProviderApi(ctx, opts, grantType)
1291
+ });
1292
+ throw new APIError("BAD_REQUEST", {
1293
+ error_description: `unsupported grant_type ${grantType}`,
1294
+ error: "unsupported_grant_type"
1295
+ });
1296
+ }
1297
+ }
1298
+ }
1299
+ /**
1300
+ * Returns the OAuth Provider's server-side capability surface bound to `ctx`.
1301
+ * The token endpoint passes one (pre-bound to the dispatched grant) to each
1302
+ * extension grant handler; a companion plugin's own endpoint calls this directly
1303
+ * with its grant type. `grantType` is bound here, not per issuance, so a handler
1304
+ * cannot mislabel the grant; omit it for capabilities that do not issue tokens
1305
+ * (`getClient`, `validateAccessToken`, `requireActiveAccessToken`), and
1306
+ * `issueTokens` then throws.
1307
+ */
1308
+ function getOAuthProviderApi(ctx, opts, grantType) {
1309
+ return {
1310
+ getClient: (clientId) => getClient(ctx, opts, clientId),
1311
+ authenticateClient: async (request) => {
1312
+ const { clientId, clientSecret, preVerified, authMethod, confirmation } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}${ctx.path ?? "/oauth2/token"}`));
1313
+ if (!clientId) throw new APIError("BAD_REQUEST", {
1314
+ error_description: "Missing required client_id",
1315
+ error: "invalid_request"
1316
+ });
1317
+ if (request?.requireCredentials !== false && !clientSecret && !preVerified) throw new APIError("BAD_REQUEST", {
1318
+ error_description: "Missing required client credentials",
1319
+ error: "invalid_client"
1320
+ });
1321
+ return {
1322
+ clientId,
1323
+ client: await validateClientCredentials(ctx, opts, clientId, clientSecret, request?.scopes, preVerified, grantType, authMethod),
1324
+ method: authMethod,
1325
+ confirmation
1326
+ };
1327
+ },
1328
+ issueTokens: (params) => {
1329
+ if (!grantType) throw new APIError("INTERNAL_SERVER_ERROR", {
1330
+ error_description: "issueTokens requires a grant type; pass it to getOAuthProviderApi(ctx, opts, grantType).",
1331
+ error: "server_error"
1332
+ });
1333
+ return createUserTokens(ctx, opts, {
1334
+ ...params,
1335
+ grantType
1336
+ });
1337
+ },
1338
+ hashToken: (token, type) => storeToken(opts.storeTokens, token, type),
1339
+ validateAccessToken: async (token, clientId) => {
1340
+ const { validateAccessToken } = await Promise.resolve().then(() => introspect_exports);
1341
+ return validateAccessToken(ctx, opts, token, clientId);
1342
+ },
1343
+ requireActiveAccessToken: async (token, clientId) => {
1344
+ const { requireActiveAccessToken } = await Promise.resolve().then(() => introspect_exports);
1345
+ return requireActiveAccessToken(ctx, opts, token, clientId);
1346
+ }
1347
+ };
1348
+ }
1349
+ async function createJwtAccessToken(ctx, opts, user, client, audienceClaim, scopes, overrides) {
1350
+ const iat = overrides?.iat ?? Math.floor(Date.now() / 1e3);
1351
+ const exp = overrides?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
1352
+ const jwtPluginOptions = getJwtPlugin(ctx.context).options;
1353
+ const subject = user?.id ?? client.clientId;
1354
+ return signJWT(ctx, {
1355
+ options: jwtPluginOptions,
1356
+ header: { typ: JWT_ACCESS_TOKEN_TYPE },
1357
+ signingKeyId: overrides?.signingKeyId ?? void 0,
1358
+ signingAlgorithm: overrides?.signingAlgorithm ?? void 0,
1359
+ payload: {
1360
+ ...overrides?.accessTokenClaims ?? {},
1361
+ sub: subject,
1362
+ aud: toAudienceClaim(audienceClaim),
1363
+ client_id: client.clientId,
1364
+ azp: client.clientId,
1365
+ scope: scopes.join(" "),
1366
+ sid: overrides?.sid,
1367
+ iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
1368
+ iat,
1369
+ exp,
1370
+ jti: generateRandomString(32),
1371
+ ...overrides?.confirmation ? { cnf: overrides.confirmation } : {}
1372
+ }
1373
+ });
1374
+ }
1375
+ /**
1376
+ * Computes an OIDC hash (at_hash, c_hash) per OIDC Core §3.1.3.6.
1377
+ * Hashes the token, takes the left half, and base64url-encodes it.
1378
+ */
1379
+ async function computeOidcHash(token, signingAlg) {
1380
+ let hashAlg;
1381
+ if (signingAlg === "EdDSA") hashAlg = "SHA-512";
1382
+ else if (signingAlg.endsWith("384")) hashAlg = "SHA-384";
1383
+ else if (signingAlg.endsWith("512")) hashAlg = "SHA-512";
1384
+ else hashAlg = "SHA-256";
1385
+ const digest = new Uint8Array(await crypto.subtle.digest(hashAlg, new TextEncoder().encode(token)));
1386
+ return base64url.encode(digest.slice(0, digest.length / 2));
1387
+ }
1388
+ /**
1389
+ * Creates a user id token in code_authorization with scope of 'openid'
1390
+ * and hybrid/implicit (not yet implemented) flows
1391
+ */
1392
+ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken, extraClaims) {
1393
+ const iat = Math.floor(Date.now() / 1e3);
1394
+ const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
1395
+ const resolvedSub = await resolveSubjectIdentifier(user.id, client, opts);
1396
+ const authTimeSec = authTime != null ? Math.floor(authTime.getTime() / 1e3) : void 0;
1397
+ const customClaims = stripReservedIdTokenClaims(opts.customIdTokenClaims ? await opts.customIdTokenClaims({
1398
+ user,
1399
+ scopes,
1400
+ metadata: parseClientMetadata(client.metadata)
1401
+ }) : void 0);
1402
+ const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
1403
+ const resolvedKey = !opts.disableJwtPlugin && !jwtPluginOptions?.jwt?.sign ? await resolveSigningKey(ctx, jwtPluginOptions) : void 0;
1404
+ const signingAlg = opts.disableJwtPlugin ? "HS256" : resolvedKey?.alg ?? jwtPluginOptions?.jwks?.keyPairConfig?.alg;
1405
+ const atHash = accessToken ? await computeOidcHash(accessToken, signingAlg) : void 0;
1406
+ const emitSid = Boolean(client.enableEndSession || client.backchannelLogoutUri);
1407
+ const payload = {
1408
+ ...ID_TOKEN_SCOPE_CLAIM_GUARDS,
1409
+ auth_time: authTimeSec,
1410
+ acr: "0",
1411
+ ...customClaims,
1412
+ at_hash: atHash,
1413
+ iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
1414
+ sub: resolvedSub,
1415
+ aud: client.clientId,
1416
+ nonce,
1417
+ iat,
1418
+ exp,
1419
+ sid: emitSid ? sessionId : void 0
1420
+ };
1421
+ const additiveClaims = stripReservedIdTokenClaims(extraClaims);
1422
+ Object.assign(payload, pickClaims(additiveClaims, payload));
1423
+ if (opts.disableJwtPlugin && !client.clientSecret) return;
1424
+ const idToken = opts.disableJwtPlugin ? await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(new TextEncoder().encode(await decryptStoredClientSecret(ctx, opts.storeClientSecret, client.clientSecret))) : await signJWT(ctx, {
1425
+ options: jwtPluginOptions,
1426
+ payload,
1427
+ resolvedKey: resolvedKey ?? void 0
1428
+ });
1429
+ if (idToken && atHash && jwtPluginOptions?.jwt?.sign) {
1430
+ const header = decodeProtectedHeader(idToken);
1431
+ if (header.alg !== signingAlg) throw new APIError("INTERNAL_SERVER_ERROR", {
1432
+ error_description: `ID token signed with "${header.alg}" but at_hash was computed for "${signingAlg}". Ensure jwt.sign uses the algorithm declared in keyPairConfig.alg.`,
1433
+ error: "server_error"
1434
+ });
1435
+ }
1436
+ return idToken;
1437
+ }
1438
+ /**
1439
+ * Encodes a refresh token for a client
1440
+ */
1441
+ async function encodeRefreshToken(opts, token, sessionId) {
1442
+ return (opts.prefix?.refreshToken ?? "") + (opts.formatRefreshToken?.encrypt ? opts.formatRefreshToken.encrypt(token, sessionId) : token);
1443
+ }
1444
+ /**
1445
+ * Decodes a refresh token for a client
1446
+ *
1447
+ * @internal
1448
+ */
1449
+ async function decodeRefreshToken(opts, token) {
1450
+ if (opts.prefix?.refreshToken) if (token.startsWith(opts.prefix.refreshToken)) token = token.replace(opts.prefix.refreshToken, "");
1451
+ else throw new APIError("BAD_REQUEST", {
1452
+ error_description: "refresh token not found",
1453
+ error: "invalid_token"
1454
+ });
1455
+ return opts.formatRefreshToken?.decrypt ? opts.formatRefreshToken?.decrypt(token) : { token };
1456
+ }
1457
+ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, authorizationCodeId, refreshId, confirmation, requestedUserInfoClaims) {
1458
+ const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
1459
+ const exp = payload?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
1460
+ const token = opts.generateOpaqueAccessToken ? await opts.generateOpaqueAccessToken() : generateRandomString(32, "A-Z", "a-z");
1461
+ await ctx.context.adapter.create({
1462
+ model: "oauthAccessToken",
1463
+ data: {
1464
+ token: await storeToken(opts.storeTokens, token, "access_token"),
1465
+ clientId: client.clientId,
1466
+ sessionId: payload?.sid,
1467
+ userId: user?.id,
1468
+ referenceId,
1469
+ authorizationCodeId,
1470
+ resources,
1471
+ refreshId,
1472
+ confirmation,
1473
+ requestedUserInfoClaims: requestedUserInfoClaims?.length ? requestedUserInfoClaims : void 0,
1474
+ scopes,
1475
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3),
1476
+ expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
1477
+ }
1478
+ });
1479
+ return (opts.prefix?.opaqueAccessToken ?? "") + token;
1480
+ }
1481
+ /**
1482
+ * Tear down the entire refresh-token family for a (client, user) pair, plus
1483
+ * any access tokens that reference those refresh rows, per RFC 9700 §4.14.
1484
+ * Access tokens are deleted first so the parent rows' foreign-key children
1485
+ * do not block the refresh-row delete.
1486
+ *
1487
+ * TODO(invalidate-family-race): the two `deleteMany` calls are not atomic
1488
+ * with respect to each other. Between them, a concurrent rotation in a
1489
+ * different worker can `create` a fresh refresh row (and, immediately after,
1490
+ * an access-token row referencing it) for the same (client, user) pair,
1491
+ * leaving the family partially rebuilt and the new refresh row orphaned of
1492
+ * any deletion. Closing this window requires the same transactional adapter
1493
+ * contract tracked under FIXME(strict-family-invalidation) in
1494
+ * `createRefreshToken`.
1495
+ *
1496
+ * @internal
1497
+ */
1498
+ async function invalidateRefreshFamily(ctx, clientId, userId) {
1499
+ const refreshTokens = await ctx.context.adapter.findMany({
1500
+ model: "oauthRefreshToken",
1501
+ where: [{
1502
+ field: "clientId",
1503
+ value: clientId
1504
+ }, {
1505
+ field: "userId",
1506
+ value: userId
1507
+ }]
1508
+ });
1509
+ if (refreshTokens.length) await ctx.context.adapter.deleteMany({
1510
+ model: "oauthAccessToken",
1511
+ where: [{
1512
+ field: "refreshId",
1513
+ operator: "in",
1514
+ value: refreshTokens.map((r) => r.id)
1515
+ }]
1516
+ });
1517
+ await ctx.context.adapter.deleteMany({
1518
+ model: "oauthRefreshToken",
1519
+ where: [{
1520
+ field: "clientId",
1521
+ value: clientId
1522
+ }, {
1523
+ field: "userId",
1524
+ value: userId
1525
+ }]
1526
+ });
1527
+ }
1528
+ async function revokeTokensIssuedForAuthorizationCode(ctx, authorizationCodeId) {
1529
+ const deleteIssuedTokens = async (model) => {
1530
+ try {
1531
+ await ctx.context.adapter.deleteMany({
1532
+ model,
1533
+ where: [{
1534
+ field: "authorizationCodeId",
1535
+ value: authorizationCodeId
1536
+ }]
1537
+ });
1538
+ } catch (error) {
1539
+ ctx.context.logger.error("authorization code replay cleanup failed", error);
1540
+ }
1541
+ };
1542
+ await deleteIssuedTokens("oauthAccessToken");
1543
+ await deleteIssuedTokens("oauthRefreshToken");
1544
+ }
1545
+ async function createRefreshToken(ctx, opts, user, referenceId, authorizationCodeId, client, scopes, payload, originalRefresh, authTime, resources, confirmation, requestedUserInfoClaims) {
1546
+ const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
1547
+ const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
1548
+ const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
1549
+ const sessionId = payload?.sid;
1550
+ const newRow = {
1551
+ token: await storeToken(opts.storeTokens, token, "refresh_token"),
1552
+ clientId: client.clientId,
1553
+ sessionId,
1554
+ userId: user.id,
1555
+ referenceId,
1556
+ authorizationCodeId,
1557
+ authTime,
1558
+ confirmation,
1559
+ requestedUserInfoClaims: requestedUserInfoClaims?.length ? requestedUserInfoClaims : void 0,
1560
+ scopes,
1561
+ resources,
1562
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3),
1563
+ expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
1564
+ };
1565
+ if (!originalRefresh?.id) return {
1566
+ id: (await ctx.context.adapter.create({
1567
+ model: "oauthRefreshToken",
1568
+ data: newRow
1569
+ })).id,
1570
+ token: await encodeRefreshToken(opts, token, sessionId)
1571
+ };
1572
+ const rotatedAt = /* @__PURE__ */ new Date(iat * 1e3);
1573
+ const rotationUpdate = {
1574
+ revoked: rotatedAt,
1575
+ rotatedAt
1576
+ };
1577
+ const reuseInterval = opts.refreshTokenReuseInterval ?? 0;
1578
+ if (reuseInterval > 0) rotationUpdate.rotationReplayExpiresAt = /* @__PURE__ */ new Date((iat + reuseInterval) * 1e3);
1579
+ if (!await ctx.context.adapter.incrementOne({
1580
+ model: "oauthRefreshToken",
1581
+ where: [{
1582
+ field: "id",
1583
+ value: originalRefresh.id
1584
+ }, {
1585
+ field: "revoked",
1586
+ operator: "eq",
1587
+ value: null
1588
+ }],
1589
+ increment: {},
1590
+ set: rotationUpdate
1591
+ })) throw new APIError("BAD_REQUEST", {
1592
+ error_description: "invalid refresh token",
1593
+ error: "invalid_grant"
1594
+ });
1595
+ return {
1596
+ id: (await ctx.context.adapter.create({
1597
+ model: "oauthRefreshToken",
1598
+ data: newRow
1599
+ })).id,
1600
+ token: await encodeRefreshToken(opts, token, sessionId)
1601
+ };
1602
+ }
1603
+ async function resolveResourceGrantIssuance(ctx, opts, params) {
1604
+ const resourcePolicy = await resolveResourcePolicy(ctx, opts, {
1605
+ resource: params.resources,
1606
+ clientId: params.clientId,
1607
+ requestedScopes: params.requestedScopes
1608
+ });
1609
+ const resourceExpiresAtSeconds = resourcePolicy.accessTokenTtl !== null ? params.iat + resourcePolicy.accessTokenTtl : params.scopeExpiresAtSeconds;
1610
+ const refreshTokenDefaultTtl = opts.refreshTokenExpiresIn ?? 2592e3;
1611
+ const refreshTokenTtl = resourcePolicy.refreshTokenTtl !== null ? Math.min(resourcePolicy.refreshTokenTtl, refreshTokenDefaultTtl) : refreshTokenDefaultTtl;
1612
+ return {
1613
+ audienceClaim: resourcePolicy.audienceClaim,
1614
+ effectiveScopes: resourcePolicy.effectiveScopes,
1615
+ accessTokenExpiresAtSeconds: Math.min(params.scopeExpiresAtSeconds, resourceExpiresAtSeconds),
1616
+ refreshTokenExpiresAtSeconds: params.iat + refreshTokenTtl,
1617
+ refreshResources: params.refreshToken?.resources ?? params.originalResources ?? params.resources,
1618
+ signingAlgorithm: resourcePolicy.signingAlgorithm,
1619
+ signingKeyId: resourcePolicy.signingKeyId,
1620
+ resourceCustomClaims: resourcePolicy.rawCustomClaims,
1621
+ dpopBoundAccessTokensRequired: resourcePolicy.dpopBoundAccessTokensRequired
1622
+ };
1623
+ }
1624
+ function throwInvalidDpopProof(errorDescription) {
1625
+ throw new APIError("BAD_REQUEST", {
1626
+ error: "invalid_dpop_proof",
1627
+ error_description: errorDescription
1628
+ });
1629
+ }
1630
+ function clientRequiresDpopBoundAccessTokens(client) {
1631
+ const metadata = parseClientMetadata(client.metadata) ?? {};
1632
+ return client.dpopBoundAccessTokens === true || metadata.dpop_bound_access_tokens === true;
1633
+ }
1634
+ async function resolveDpopTokenBinding(ctx, opts, params) {
1635
+ const authCodeDpopJkt = params.verificationValue?.query.dpop_jkt;
1636
+ const refreshJkt = getConfirmationJkt(params.refreshToken?.confirmation);
1637
+ const expectedJkt = refreshJkt ?? authCodeDpopJkt;
1638
+ const dpopProofJwt = getDpopProofJwt(ctx);
1639
+ const dpopRequired = clientRequiresDpopBoundAccessTokens(params.client) || params.grantIssuance.dpopBoundAccessTokensRequired || !!authCodeDpopJkt || !!refreshJkt;
1640
+ if (!dpopProofJwt) {
1641
+ if (dpopRequired) throwInvalidDpopProof("DPoP proof header is required");
1642
+ return;
1643
+ }
1644
+ try {
1645
+ return { jkt: (await verifyDpopProof({
1646
+ proofJwt: dpopProofJwt,
1647
+ method: "POST",
1648
+ url: getEndpointUrl(ctx, "/oauth2/token"),
1649
+ expectedJkt,
1650
+ proofMaxAgeSeconds: opts.dpop?.proofMaxAgeSeconds,
1651
+ signingAlgorithms: opts.dpop?.signingAlgorithms,
1652
+ replayStore: createDpopReplayStore(ctx.context.internalAdapter)
1653
+ })).jkt };
1654
+ } catch (error) {
1655
+ if (isDpopProofError(error)) throwInvalidDpopProof(error.message);
1656
+ throw error;
1657
+ }
1658
+ }
1659
+ function normalizeReplayValues(values) {
1660
+ return values ? [...new Set(values)].sort() : void 0;
1661
+ }
1662
+ function sameReplayValues(left, right) {
1663
+ const normalizedLeft = normalizeReplayValues(left);
1664
+ const normalizedRight = normalizeReplayValues(right);
1665
+ if (normalizedLeft === void 0 || normalizedRight === void 0) return normalizedLeft === normalizedRight;
1666
+ if (normalizedLeft.length !== normalizedRight.length) return false;
1667
+ return normalizedLeft.every((value, index) => value === normalizedRight[index]);
1668
+ }
1669
+ function confirmationReplayKey(confirmation) {
1670
+ if (!confirmation) return;
1671
+ if ("jkt" in confirmation) return `jkt:${confirmation.jkt}`;
1672
+ return `x5t#S256:${confirmation["x5t#S256"]}`;
1673
+ }
1674
+ function sameConfirmation(left, right) {
1675
+ return confirmationReplayKey(left) === confirmationReplayKey(right);
1676
+ }
1677
+ function isConfirmation(value) {
1678
+ if (!value || typeof value !== "object") return false;
1679
+ const confirmation = value;
1680
+ return typeof confirmation.jkt === "string" && confirmation["x5t#S256"] === void 0 || typeof confirmation["x5t#S256"] === "string" && confirmation.jkt === void 0;
1681
+ }
1682
+ function isTokenType(value) {
1683
+ return value === "Bearer" || value === "DPoP";
1684
+ }
1685
+ function isOAuthTokenResponse(value) {
1686
+ if (!value || typeof value !== "object") return false;
1687
+ const response = value;
1688
+ return typeof response.access_token === "string" && typeof response.expires_in === "number" && typeof response.expires_at === "number" && isTokenType(response.token_type) && typeof response.scope === "string" && (response.refresh_token === void 0 || typeof response.refresh_token === "string") && (response.id_token === void 0 || typeof response.id_token === "string");
1689
+ }
1690
+ function isRefreshTokenRotationReplay(value) {
1691
+ if (!value || typeof value !== "object") return false;
1692
+ const replay = value;
1693
+ const request = replay.request;
1694
+ return !!request && Array.isArray(request.effectiveScopes) && request.effectiveScopes.every((scope) => typeof scope === "string") && (request.requestedResources === void 0 || Array.isArray(request.requestedResources) && request.requestedResources.every((resource) => typeof resource === "string")) && (request.confirmation === void 0 || isConfirmation(request.confirmation)) && isOAuthTokenResponse(replay.response);
1695
+ }
1696
+ function buildRefreshTokenRotationReplayRequest(params) {
1697
+ const requestedResources = normalizeReplayValues(params.requestedResources);
1698
+ return {
1699
+ effectiveScopes: normalizeReplayValues(params.effectiveScopes) ?? [],
1700
+ ...requestedResources ? { requestedResources } : {},
1701
+ ...params.confirmation ? { confirmation: params.confirmation } : {}
1702
+ };
1703
+ }
1704
+ function sameRefreshTokenRotationReplayRequest(left, right) {
1705
+ return sameReplayValues(left.effectiveScopes, right.effectiveScopes) && sameReplayValues(left.requestedResources, right.requestedResources) && sameConfirmation(left.confirmation, right.confirmation);
1706
+ }
1707
+ async function encryptRefreshTokenRotationReplay(ctx, replay) {
1708
+ return symmetricEncrypt({
1709
+ key: ctx.context.secretConfig,
1710
+ data: JSON.stringify(replay)
1711
+ });
1712
+ }
1713
+ async function decryptRefreshTokenRotationReplay(ctx, value) {
1714
+ const decrypted = await symmetricDecrypt({
1715
+ key: ctx.context.secretConfig,
1716
+ data: value
1717
+ });
1718
+ const parsed = JSON.parse(decrypted);
1719
+ if (!isRefreshTokenRotationReplay(parsed)) return;
1720
+ return parsed;
1721
+ }
1722
+ function isWithinRefreshTokenReuseInterval(refreshToken) {
1723
+ return !!refreshToken.rotatedAt && !!refreshToken.rotationReplayExpiresAt && refreshToken.rotationReplayExpiresAt >= /* @__PURE__ */ new Date();
1724
+ }
1725
+ async function storeRefreshTokenRotationReplay(ctx, opts, refreshToken, request, response) {
1726
+ if ((opts.refreshTokenReuseInterval ?? 0) <= 0) return;
1727
+ await ctx.context.adapter.update({
1728
+ model: "oauthRefreshToken",
1729
+ where: [{
1730
+ field: "id",
1731
+ value: refreshToken.id
1732
+ }],
1733
+ update: { rotationReplayResponse: await encryptRefreshTokenRotationReplay(ctx, {
1734
+ request,
1735
+ response
1736
+ }) }
1737
+ });
1738
+ }
1739
+ async function getRefreshTokenRotationReplay(ctx, refreshToken, request) {
1740
+ if (!isWithinRefreshTokenReuseInterval(refreshToken)) return;
1741
+ if (!refreshToken.rotationReplayResponse) return;
1742
+ try {
1743
+ const replay = await decryptRefreshTokenRotationReplay(ctx, refreshToken.rotationReplayResponse);
1744
+ if (!replay || !sameRefreshTokenRotationReplayRequest(replay.request, request)) return;
1745
+ return replay.response;
1746
+ } catch (error) {
1747
+ ctx.context.logger.error("refresh token rotation replay failed", error);
1748
+ return;
1749
+ }
1750
+ }
1751
+ async function resolveRefreshTokenRotationReplayRequest(ctx, opts, params) {
1752
+ const iat = Math.floor(Date.now() / 1e3);
1753
+ const grantIssuance = await resolveResourceGrantIssuance(ctx, opts, {
1754
+ clientId: params.client.clientId,
1755
+ requestedScopes: params.scopes,
1756
+ resources: params.resources,
1757
+ refreshToken: params.refreshToken,
1758
+ iat,
1759
+ scopeExpiresAtSeconds: iat + (opts.accessTokenExpiresIn ?? 3600)
1760
+ });
1761
+ const confirmation = await resolveDpopTokenBinding(ctx, opts, {
1762
+ client: params.client,
1763
+ grantIssuance,
1764
+ refreshToken: params.refreshToken
1765
+ });
1766
+ return buildRefreshTokenRotationReplayRequest({
1767
+ effectiveScopes: grantIssuance.effectiveScopes,
1768
+ requestedResources: params.resources,
1769
+ confirmation
1770
+ });
1771
+ }
1772
+ function replayRefreshTokenRotationResponse(ctx, response) {
1773
+ const now = Math.floor(Date.now() / 1e3);
1774
+ return ctx.json({
1775
+ ...response,
1776
+ expires_in: Math.max(0, response.expires_at - now)
1777
+ });
1778
+ }
1779
+ async function createUserTokens(ctx, opts, params) {
1780
+ const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, verificationValue } = params;
1781
+ const iat = Math.floor(Date.now() / 1e3);
1782
+ const defaultExp = iat + (user ? opts.accessTokenExpiresIn ?? 3600 : opts.m2mAccessTokenExpiresIn ?? 3600);
1783
+ const scopeExp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
1784
+ return prev < curr ? prev : curr;
1785
+ }, defaultExp) : defaultExp;
1786
+ const grantIssuance = await resolveResourceGrantIssuance(ctx, opts, {
1787
+ clientId: client.clientId,
1788
+ requestedScopes: scopes,
1789
+ resources: params.resources,
1790
+ originalResources: params.originalResources,
1791
+ refreshToken: params.refreshToken,
1792
+ iat,
1793
+ scopeExpiresAtSeconds: scopeExp
1794
+ });
1795
+ const audienceClaim = grantIssuance.audienceClaim;
1796
+ const effectiveScopes = grantIssuance.effectiveScopes;
1797
+ const exp = grantIssuance.accessTokenExpiresAtSeconds;
1798
+ const refreshTokenExp = grantIssuance.refreshTokenExpiresAtSeconds;
1799
+ const isRefreshToken = user && clientAllowsGrant(client, "refresh_token") && (existingRefreshToken?.scopes?.includes("offline_access") || scopes.includes("offline_access"));
1800
+ const isJwtAccessToken = audienceClaim && !opts.disableJwtPlugin;
1801
+ const isIdToken = user && effectiveScopes.includes("openid");
1802
+ const metadata = parseClientMetadata(client.metadata);
1803
+ const additionalIdTokenClaims = isIdToken && user ? {
1804
+ ...await collectExtensionIdTokenClaims(opts, {
1805
+ ctx,
1806
+ opts,
1807
+ user,
1808
+ client,
1809
+ scopes: effectiveScopes,
1810
+ grantType,
1811
+ referenceId,
1812
+ sessionId,
1813
+ resources: params.resources,
1814
+ metadata
1815
+ }),
1816
+ ...params.idTokenClaims ?? {}
1817
+ } : void 0;
1818
+ const customFields = opts.customTokenResponseFields ? await opts.customTokenResponseFields({
1819
+ grantType,
1820
+ user,
1821
+ scopes: effectiveScopes,
1822
+ metadata,
1823
+ verificationValue
1824
+ }) : void 0;
1825
+ const refreshResources = grantIssuance.refreshResources;
1826
+ const confirmation = params.confirmation ?? await resolveDpopTokenBinding(ctx, opts, {
1827
+ client,
1828
+ grantIssuance,
1829
+ verificationValue,
1830
+ refreshToken: existingRefreshToken
1831
+ });
1832
+ const requestedUserInfoClaims = params.requestedUserInfoClaims ?? existingRefreshToken?.requestedUserInfoClaims ?? [];
1833
+ const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, params.authorizationCodeId, client, effectiveScopes, {
1834
+ iat,
1835
+ exp: refreshTokenExp,
1836
+ sid: sessionId
1837
+ }, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0;
1838
+ const accessTokenClaims = isJwtAccessToken ? await resolveAccessTokenClaims({
1839
+ ctx,
1840
+ opts,
1841
+ user,
1842
+ client,
1843
+ scopes: effectiveScopes,
1844
+ grantType,
1845
+ sessionId,
1846
+ resources: params.resources,
1847
+ referenceId,
1848
+ metadata,
1849
+ perRequestClaims: params.accessTokenClaims,
1850
+ resourcePolicyClaims: grantIssuance.resourceCustomClaims
1851
+ }) : void 0;
1852
+ const [accessToken, refreshToken] = await Promise.all([isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audienceClaim, effectiveScopes, {
1853
+ iat,
1854
+ exp,
1855
+ sid: sessionId,
1856
+ signingAlgorithm: grantIssuance.signingAlgorithm,
1857
+ signingKeyId: grantIssuance.signingKeyId,
1858
+ accessTokenClaims,
1859
+ confirmation
1860
+ }) : createOpaqueAccessToken(ctx, opts, user, client, effectiveScopes, {
1861
+ iat,
1862
+ exp,
1863
+ sid: sessionId
1864
+ }, params?.resources, referenceId, params.authorizationCodeId, earlyRefreshToken?.id, confirmation, requestedUserInfoClaims), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, params.authorizationCodeId, client, effectiveScopes, {
1865
+ iat,
1866
+ exp: refreshTokenExp,
1867
+ sid: sessionId
1868
+ }, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0]);
1869
+ const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, effectiveScopes, nonce, sessionId, authTime, accessToken, additionalIdTokenClaims) : void 0;
1870
+ const responseBody = {
1871
+ ...customFields,
1872
+ ...params.tokenResponse ?? {},
1873
+ access_token: accessToken,
1874
+ expires_in: exp - iat,
1875
+ expires_at: exp,
1876
+ token_type: confirmationTokenType(confirmation),
1877
+ refresh_token: refreshToken?.token,
1878
+ scope: effectiveScopes.join(" "),
1879
+ id_token: idToken
1880
+ };
1881
+ if (existingRefreshToken?.id && refreshToken?.id) try {
1882
+ await storeRefreshTokenRotationReplay(ctx, opts, existingRefreshToken, buildRefreshTokenRotationReplayRequest({
1883
+ effectiveScopes,
1884
+ requestedResources: params.resources,
1885
+ confirmation
1886
+ }), responseBody);
1887
+ } catch (error) {
1888
+ ctx.context.logger.error("failed to store refresh token rotation replay", error);
1889
+ }
1890
+ return ctx.json(responseBody);
1891
+ }
1892
+ /** Checks verification value */
1893
+ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resource) {
1894
+ const authorizationCodeId = await storeToken(opts.storeTokens, code, "authorization_code");
1895
+ const verification = await ctx.context.internalAdapter.consumeVerificationValue(authorizationCodeId);
1896
+ if (!verification) {
1897
+ await revokeTokensIssuedForAuthorizationCode(ctx, authorizationCodeId);
1898
+ throw new APIError("BAD_REQUEST", {
1899
+ error_description: "invalid code",
1900
+ error: "invalid_grant"
1901
+ });
1902
+ }
1903
+ let rawValue;
1904
+ try {
1905
+ rawValue = JSON.parse(verification.value);
1906
+ } catch {
1907
+ throw new APIError("BAD_REQUEST", {
1908
+ error_description: "malformed verification value",
1909
+ error: "invalid_grant"
1910
+ });
1911
+ }
1912
+ const parsed = verificationValueSchema.safeParse(rawValue);
1913
+ if (!parsed.success) throw new APIError("BAD_REQUEST", {
1914
+ error_description: "malformed verification value",
1915
+ error: "invalid_grant"
1916
+ });
1917
+ const verificationValue = parsed.data;
1918
+ if (verificationValue.query.client_id !== client_id) throw new APIError("BAD_REQUEST", {
1919
+ error_description: "invalid client_id",
1920
+ error: "invalid_grant"
1921
+ });
1922
+ const boundRedirectUri = verificationValue.query.redirect_uri;
1923
+ if (boundRedirectUri) {
1924
+ if (!redirect_uri) throw new APIError("BAD_REQUEST", {
1925
+ error_description: "redirect_uri is required",
1926
+ error: "invalid_request"
1927
+ });
1928
+ if (boundRedirectUri !== redirect_uri) throw new APIError("BAD_REQUEST", {
1929
+ error_description: "redirect_uri mismatch",
1930
+ error: "invalid_grant"
1931
+ });
1932
+ } else if (redirect_uri) throw new APIError("BAD_REQUEST", {
1933
+ error_description: "redirect_uri mismatch",
1934
+ error: "invalid_grant"
1935
+ });
1936
+ const storedResources = toResourceList(verificationValue.resource) ?? toResourceList(verificationValue.query.resource);
1937
+ const effectiveResources = resource ?? storedResources;
1938
+ if (resource && storedResources) {
1939
+ const requestedSet = new Set(resource);
1940
+ const authorizedSet = new Set(storedResources);
1941
+ for (const r of requestedSet) if (!authorizedSet.has(r)) throw new APIError("BAD_REQUEST", {
1942
+ error_description: "requested resource not authorized",
1943
+ error: "invalid_target"
1944
+ });
1945
+ }
1946
+ return {
1947
+ verificationValue,
1948
+ effectiveResources,
1949
+ authorizedResources: storedResources,
1950
+ authorizationCodeId
1951
+ };
1952
+ }
1953
+ /**
1954
+ * Obtains new Session Jwt and Refresh Tokens using a code
1955
+ */
1956
+ async function handleAuthorizationCodeGrant(ctx, opts) {
1957
+ const { clientId: client_id, clientSecret: client_secret, preVerified, authMethod } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
1958
+ const { code, code_verifier, redirect_uri, resource } = ctx.body;
1959
+ const resources = toResourceList(resource);
1960
+ if (!client_id) throw new APIError("BAD_REQUEST", {
1961
+ error_description: "client_id is required",
1962
+ error: "invalid_request"
1963
+ });
1964
+ if (!code) throw new APIError("BAD_REQUEST", {
1965
+ error_description: "code is required",
1966
+ error: "invalid_request"
1967
+ });
1968
+ const isAuthCodeWithSecret = client_id && client_secret;
1969
+ const isAuthCodeWithPkce = client_id && code && code_verifier;
1970
+ if (!isAuthCodeWithSecret && !isAuthCodeWithPkce && !preVerified) throw new APIError("BAD_REQUEST", {
1971
+ error_description: "Either code_verifier or client_secret is required",
1972
+ error: "invalid_request"
1973
+ });
1974
+ /** Get and check Verification Value */
1975
+ const { verificationValue, effectiveResources, authorizedResources, authorizationCodeId } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
1976
+ const scopes = verificationValue.query.scope?.split(" ");
1977
+ if (!scopes) throw new APIError("INTERNAL_SERVER_ERROR", {
1978
+ error_description: "verification scope unset",
1979
+ error: "invalid_scope"
1980
+ });
1981
+ /** Verify Client */
1982
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerified, "authorization_code", authMethod);
1983
+ if (isPKCERequired(client, {
1984
+ scopes: (verificationValue.query?.scope)?.split(" ") || [],
1985
+ nonce: verificationValue.query?.nonce
1986
+ })) {
1987
+ if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
1988
+ error_description: "PKCE is required for this client",
1989
+ error: "invalid_request"
1990
+ });
1991
+ } else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret || preVerified)) throw new APIError("BAD_REQUEST", {
1992
+ error_description: "Either PKCE (code_verifier) or client authentication (client_secret or client_assertion) is required",
1993
+ error: "invalid_request"
1994
+ });
1995
+ /** Check PKCE challenge if verifier is provided */
1996
+ const pkceUsedInAuth = !!verificationValue.query?.code_challenge;
1997
+ const pkceUsedInToken = !!code_verifier;
1998
+ if (pkceUsedInAuth || pkceUsedInToken) {
1999
+ if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
2000
+ error_description: "code_verifier required because PKCE was used in authorization",
2001
+ error: "invalid_request"
2002
+ });
2003
+ if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
2004
+ error_description: "code_verifier provided but PKCE was not used in authorization",
2005
+ error: "invalid_request"
2006
+ });
2007
+ if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("UNAUTHORIZED", {
2008
+ error_description: "code verification failed",
2009
+ error: "invalid_request"
2010
+ });
2011
+ }
2012
+ /** Get user */
2013
+ if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
2014
+ error_description: "missing user, user may have been deleted",
2015
+ error: "invalid_user"
2016
+ });
2017
+ const user = await ctx.context.internalAdapter.findUserById(verificationValue.userId);
2018
+ if (!user) throw new APIError("BAD_REQUEST", {
2019
+ error_description: "missing user, user may have been deleted",
2020
+ error: "invalid_user"
2021
+ });
2022
+ const session = await ctx.context.adapter.findOne({
2023
+ model: "session",
2024
+ where: [{
2025
+ field: "id",
2026
+ value: verificationValue.sessionId
2027
+ }]
2028
+ });
2029
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
2030
+ error_description: "session no longer exists",
2031
+ error: "invalid_request"
2032
+ });
2033
+ const authTime = verificationValue.authTime != null ? normalizeTimestampValue(verificationValue.authTime) : resolveSessionAuthTime(session);
2034
+ const requestedUserInfoClaims = getRequestedUserInfoClaims(verificationValue.query.claims, getSupportedClaims(opts));
2035
+ return createUserTokens(ctx, opts, {
2036
+ client,
2037
+ scopes: verificationValue.query.scope?.split(" ") ?? [],
2038
+ user,
2039
+ grantType: "authorization_code",
2040
+ referenceId: verificationValue.referenceId,
2041
+ sessionId: session.id,
2042
+ nonce: verificationValue.query?.nonce,
2043
+ authTime,
2044
+ verificationValue,
2045
+ authorizationCodeId,
2046
+ requestedUserInfoClaims,
2047
+ resources: effectiveResources,
2048
+ originalResources: authorizedResources
2049
+ });
2050
+ }
2051
+ /**
2052
+ * Grant that allows direct access to an API using the application's credentials
2053
+ * This grant is for M2M so the concept of a user id does not exist on the token.
2054
+ *
2055
+ * MUST follow https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
2056
+ */
2057
+ async function handleClientCredentialsGrant(ctx, opts) {
2058
+ const { clientId: client_id, clientSecret: client_secret, preVerified, authMethod } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
2059
+ const { scope, resource } = ctx.body;
2060
+ const resources = toResourceList(resource);
2061
+ if (!client_id) throw new APIError("BAD_REQUEST", {
2062
+ error_description: "Missing required client_id",
2063
+ error: "invalid_request"
2064
+ });
2065
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, "client_credentials", authMethod);
2066
+ if (client.tokenEndpointAuthMethod === "none") throw new APIError("BAD_REQUEST", {
2067
+ error_description: "public clients cannot use the client_credentials grant",
2068
+ error: "unauthorized_client"
2069
+ });
2070
+ const clientCredentialsScopes = client.clientCredentialsScopes;
2071
+ if (!clientCredentialsScopes?.length) throw new APIError("BAD_REQUEST", {
2072
+ error_description: "client has no authorized client_credentials scopes",
2073
+ error: "unauthorized_client"
2074
+ });
2075
+ let requestedScopes = scope?.split(" ");
2076
+ if (requestedScopes) {
2077
+ const validScopes = new Set(clientCredentialsScopes);
2078
+ const invalidScopes = requestedScopes.filter((scope) => {
2079
+ return !validScopes.has(scope) || isUserDelegatedScope(scope);
2080
+ });
2081
+ if (invalidScopes.length) throw new APIError("BAD_REQUEST", {
2082
+ error_description: `The following scopes are invalid: ${invalidScopes.join(", ")}`,
2083
+ error: "invalid_scope"
2084
+ });
2085
+ }
2086
+ if (!requestedScopes) requestedScopes = [...clientCredentialsScopes];
2087
+ return createUserTokens(ctx, opts, {
2088
+ client,
2089
+ scopes: requestedScopes,
2090
+ grantType: "client_credentials",
2091
+ resources
2092
+ });
2093
+ }
2094
+ /**
2095
+ * Obtains new Session Jwt and Refresh Tokens using a refresh token
2096
+ *
2097
+ * Refresh tokens will only allow the same or lesser scopes as the initial authorize request.
2098
+ * To add scopes, you must restart the authorize process again.
2099
+ */
2100
+ async function handleRefreshTokenGrant(ctx, opts) {
2101
+ const { clientId: client_id, clientSecret: client_secret, preVerified, authMethod } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
2102
+ const { refresh_token, scope, resource } = ctx.body;
2103
+ const resources = toResourceList(resource);
2104
+ if (!client_id) throw new APIError("BAD_REQUEST", {
2105
+ error_description: "Missing required client_id",
2106
+ error: "invalid_request"
2107
+ });
2108
+ if (!refresh_token) throw new APIError("BAD_REQUEST", {
2109
+ error_description: "Missing a required refresh_token for refresh_token grant",
2110
+ error: "invalid_request"
2111
+ });
2112
+ const decodedRefresh = await decodeRefreshToken(opts, refresh_token);
2113
+ const refreshToken = await ctx.context.adapter.findOne({
2114
+ model: "oauthRefreshToken",
2115
+ where: [{
2116
+ field: "token",
2117
+ value: await getStoredToken(opts.storeTokens, decodedRefresh.token, "refresh_token")
2118
+ }]
2119
+ });
2120
+ if (!refreshToken) throw new APIError("BAD_REQUEST", {
2121
+ error_description: "session not found",
2122
+ error: "invalid_grant"
2123
+ });
2124
+ if (refreshToken.clientId !== client_id) throw new APIError("BAD_REQUEST", {
2125
+ error_description: "invalid refresh token",
2126
+ error: "invalid_grant"
2127
+ });
2128
+ if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
2129
+ error_description: "invalid refresh token",
2130
+ error: "invalid_grant"
2131
+ });
2132
+ if (resources && refreshToken.resources && !resources.every((v) => refreshToken.resources?.includes(v))) throw new APIError("BAD_REQUEST", {
2133
+ error_description: "requested resource invalid",
2134
+ error: "invalid_target"
2135
+ });
2136
+ const scopes = refreshToken?.scopes;
2137
+ const requestedScopes = scope?.split(" ");
2138
+ if (requestedScopes) {
2139
+ const validScopes = new Set(scopes);
2140
+ for (const requestedScope of requestedScopes) if (!validScopes.has(requestedScope)) throw new APIError("BAD_REQUEST", {
2141
+ error_description: `unable to issue scope ${requestedScope}`,
2142
+ error: "invalid_scope"
2143
+ });
2144
+ }
2145
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes, preVerified, "refresh_token", authMethod);
2146
+ if (refreshToken.revoked) {
2147
+ if (isWithinRefreshTokenReuseInterval(refreshToken)) {
2148
+ const replay = await getRefreshTokenRotationReplay(ctx, refreshToken, await resolveRefreshTokenRotationReplayRequest(ctx, opts, {
2149
+ client,
2150
+ refreshToken,
2151
+ scopes: requestedScopes ?? scopes,
2152
+ resources: resources ?? refreshToken.resources
2153
+ }));
2154
+ if (replay) return replayRefreshTokenRotationResponse(ctx, replay);
2155
+ throw new APIError("BAD_REQUEST", {
2156
+ error_description: "invalid refresh token",
2157
+ error: "invalid_grant"
2158
+ });
2159
+ }
2160
+ await invalidateRefreshFamily(ctx, client_id, refreshToken.userId);
2161
+ throw new APIError("BAD_REQUEST", {
2162
+ error_description: "invalid refresh token",
2163
+ error: "invalid_grant"
2164
+ });
2165
+ }
2166
+ const user = await ctx.context.internalAdapter.findUserById(refreshToken.userId);
2167
+ if (!user) throw new APIError("BAD_REQUEST", {
2168
+ error_description: "user not found",
2169
+ error: "invalid_request"
2170
+ });
2171
+ const authTime = refreshToken.authTime != null ? normalizeTimestampValue(refreshToken.authTime) : void 0;
2172
+ return createUserTokens(ctx, opts, {
2173
+ client,
2174
+ scopes: requestedScopes ?? scopes,
2175
+ user,
2176
+ grantType: "refresh_token",
2177
+ referenceId: refreshToken.referenceId,
2178
+ authorizationCodeId: refreshToken.authorizationCodeId,
2179
+ sessionId: refreshToken.sessionId,
2180
+ refreshToken,
2181
+ resources: resources ?? refreshToken.resources,
2182
+ authTime,
2183
+ requestedUserInfoClaims: refreshToken.requestedUserInfoClaims
2184
+ });
2185
+ }
2186
+ //#endregion
2187
+ //#region src/introspect.ts
2188
+ var introspect_exports = /* @__PURE__ */ __exportAll({
2189
+ introspectEndpoint: () => introspectEndpoint,
2190
+ requireActiveAccessToken: () => requireActiveAccessToken,
2191
+ requireActiveAccessTokenWithClaims: () => requireActiveAccessTokenWithClaims,
2192
+ validateAccessToken: () => validateAccessToken
2193
+ });
2194
+ const INVALID_ACCESS_TOKEN_ERROR_DESCRIPTION = "Invalid access token";
2195
+ const INVALID_ACCESS_TOKEN_WWW_AUTHENTICATE = `Bearer error="invalid_token", error_description="${INVALID_ACCESS_TOKEN_ERROR_DESCRIPTION}"`;
2196
+ function inactiveAccessToken() {
2197
+ return {
2198
+ payload: { active: false },
2199
+ requestedUserInfoClaims: []
2200
+ };
2201
+ }
2202
+ /**
2203
+ * IMPORTANT NOTES:
2204
+ * Introspection follows RFC7662
2205
+ * https://datatracker.ietf.org/doc/html/rfc7662
2206
+ * - APIError: Continue catches (returnable to client)
2207
+ * - Error: Should immediately stop catches (internal error)
2208
+ */
2209
+ /**
2210
+ * Resource identifiers in a token's audience, excluding the implicit
2211
+ * `/oauth2/userinfo` audience (which is not a configured resource server).
2212
+ */
2213
+ function audienceResourceIdentifiers(aud, userInfoAud) {
2214
+ if (aud == null) return [];
2215
+ return (Array.isArray(aud) ? aud : [aud]).filter((value) => typeof value === "string" && value !== userInfoAud);
2216
+ }
2217
+ /**
2218
+ * Authorizes an introspection request (RFC 7662 §2.1/§4). The caller is already
2219
+ * authenticated as a registered client; it may introspect the token when it
2220
+ * issued the token, or when it is a resource server linked to one of the token's
2221
+ * audience resources. Tokens with no resource audience stay issuer-only. An
2222
+ * unauthorized caller receives `{active:false}` (indistinguishable from an
2223
+ * expired or unknown token), preserving the §4 anti-scanning property.
2224
+ */
2225
+ async function isIntrospectionAuthorized(ctx, opts, params) {
2226
+ const { introspectingClientId, issuerClientId, audienceResources } = params;
2227
+ if (!introspectingClientId) return true;
2228
+ if (introspectingClientId === issuerClientId) return true;
2229
+ if (audienceResources.length === 0) return false;
2230
+ return isClientLinkedToAnyResource(ctx, opts, introspectingClientId, audienceResources);
2231
+ }
2232
+ /**
2233
+ * Validates a JWT access token against the configured JWKs.
2234
+ *
2235
+ * @returns RFC7662 introspection format
2236
+ */
2237
+ async function validateJwtAccessToken(ctx, opts, token, clientId) {
2238
+ const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
2239
+ const jwtPluginOptions = jwtPlugin?.options;
2240
+ const userInfoAud = `${ctx.context.baseURL ?? ""}/oauth2/userinfo`;
2241
+ const expectedIssuer = jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL;
2242
+ let jwtPayload;
2243
+ try {
2244
+ jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
2245
+ jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
2246
+ return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
2247
+ },
2248
+ jwksCacheKey: jwtPlugin
2249
+ })), { issuer: expectedIssuer })).payload;
2250
+ } catch (error) {
2251
+ if (error instanceof Error) {
2252
+ if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
2253
+ error_description: "invalid JWT signature",
2254
+ error: "invalid_request"
2255
+ });
2256
+ else if (error.name === "JWTExpired") return { active: false };
2257
+ else if (error.name === "JWTInvalid") return { active: false };
2258
+ throw error;
2259
+ }
2260
+ throw new Error(error);
2261
+ }
2262
+ const rawAud = jwtPayload.aud;
2263
+ if (!await isAudienceClaimAllowed(ctx, opts, rawAud, [userInfoAud])) return { active: false };
2264
+ if (!jwtPayload.azp) return { active: false };
2265
+ const client = await getClient(ctx, opts, jwtPayload.azp);
2266
+ if (!client || client?.disabled) return { active: false };
2267
+ if (!await isIntrospectionAuthorized(ctx, opts, {
2268
+ introspectingClientId: clientId,
2269
+ issuerClientId: jwtPayload.azp,
2270
+ audienceResources: audienceResourceIdentifiers(rawAud, userInfoAud)
2271
+ })) return { active: false };
2272
+ const sessionId = jwtPayload.sid;
2273
+ if (sessionId) {
2274
+ const session = await ctx.context.adapter.findOne({
2275
+ model: "session",
2276
+ where: [{
2277
+ field: "id",
2278
+ value: sessionId
2279
+ }]
2280
+ });
2281
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
2282
+ }
2283
+ jwtPayload.client_id = jwtPayload.azp;
2284
+ jwtPayload.active = true;
2285
+ jwtPayload.token_type = confirmationTokenType(jwtPayload.cnf);
2286
+ return jwtPayload;
2287
+ }
2288
+ /**
2289
+ * Searches for an opaque access token in the database and validates it
2290
+ *
2291
+ * @returns RFC7662 introspection format
2292
+ */
2293
+ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
2294
+ let tokenValue = token;
2295
+ if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
2296
+ else throw new APIError$1("BAD_REQUEST", {
2297
+ error_description: "opaque access token not found",
2298
+ error: "invalid_request"
2299
+ });
2300
+ const accessToken = await ctx.context.adapter.findOne({
2301
+ model: "oauthAccessToken",
2302
+ where: [{
2303
+ field: "token",
2304
+ value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
2305
+ }]
2306
+ });
2307
+ if (!accessToken) throw new APIError$1("BAD_REQUEST", {
2308
+ error_description: "opaque access token not found",
2309
+ error: "invalid_token"
2310
+ });
2311
+ if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return inactiveAccessToken();
2312
+ if (accessToken.revoked) return inactiveAccessToken();
2313
+ const resources = Array.isArray(accessToken.resources) ? accessToken.resources : void 0;
2314
+ let client;
2315
+ if (accessToken.clientId) {
2316
+ client = await getClient(ctx, opts, accessToken.clientId);
2317
+ if (!client || client?.disabled) return inactiveAccessToken();
2318
+ if (!await isIntrospectionAuthorized(ctx, opts, {
2319
+ introspectingClientId: clientId,
2320
+ issuerClientId: accessToken.clientId,
2321
+ audienceResources: resources ?? []
2322
+ })) return inactiveAccessToken();
2323
+ }
2324
+ const sessionId = accessToken.sessionId ?? void 0;
2325
+ if (sessionId) {
2326
+ const session = await ctx.context.adapter.findOne({
2327
+ model: "session",
2328
+ where: [{
2329
+ field: "id",
2330
+ value: sessionId
2331
+ }]
2332
+ });
2333
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) return inactiveAccessToken();
2334
+ }
2335
+ let user;
2336
+ if (accessToken.userId) user = await ctx.context.internalAdapter.findUserById(accessToken?.userId);
2337
+ const userInfoEndpoint = `${ctx.context.baseURL}/oauth2/userinfo`;
2338
+ if (resources?.length && !await isAudienceClaimAllowed(ctx, opts, resources, [userInfoEndpoint])) return inactiveAccessToken();
2339
+ const audienceClaim = resources ? [...resources] : void 0;
2340
+ if (audienceClaim?.length && accessToken.scopes?.includes("openid")) {
2341
+ if (!audienceClaim.includes(userInfoEndpoint)) audienceClaim.push(userInfoEndpoint);
2342
+ }
2343
+ const resourcePolicyClaims = resources?.length ? await getResourceCustomClaims(ctx, opts, resources) : {};
2344
+ const accessTokenClaims = client ? await resolveAccessTokenClaims({
2345
+ ctx,
2346
+ opts,
2347
+ user,
2348
+ client,
2349
+ scopes: accessToken.scopes ?? [],
2350
+ grantType: void 0,
2351
+ sessionId: void 0,
2352
+ resources,
2353
+ referenceId: accessToken.referenceId,
2354
+ metadata: parseClientMetadata(client.metadata),
2355
+ perRequestClaims: void 0,
2356
+ resourcePolicyClaims
2357
+ }) : {};
2358
+ const jwtPluginOptions = (opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options;
2359
+ return {
2360
+ payload: {
2361
+ ...accessTokenClaims,
2362
+ active: true,
2363
+ iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
2364
+ aud: toAudienceClaim(audienceClaim),
2365
+ client_id: accessToken.clientId,
2366
+ azp: accessToken.clientId,
2367
+ sub: user?.id,
2368
+ sid: sessionId,
2369
+ exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
2370
+ iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
2371
+ scope: accessToken.scopes?.join(" "),
2372
+ token_type: confirmationTokenType(accessToken.confirmation),
2373
+ ...accessToken.confirmation ? { cnf: accessToken.confirmation } : {}
2374
+ },
2375
+ requestedUserInfoClaims: accessToken.requestedUserInfoClaims ?? []
2376
+ };
2377
+ }
2378
+ /**
2379
+ * Validates a refresh token in the session store.
2380
+ *
2381
+ * @returns payload in RFC7662 introspection format
2382
+ */
2383
+ async function validateRefreshToken(ctx, opts, token, clientId) {
2384
+ const refreshToken = await ctx.context.adapter.findOne({
2385
+ model: "oauthRefreshToken",
2386
+ where: [{
2387
+ field: "token",
2388
+ value: await getStoredToken(opts.storeTokens, token, "refresh_token")
2389
+ }]
2390
+ });
2391
+ if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
2392
+ error_description: "token not found",
2393
+ error: "invalid_token"
2394
+ });
2395
+ if (!refreshToken.clientId || refreshToken.clientId !== clientId) return { active: false };
2396
+ if (!refreshToken.expiresAt || refreshToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
2397
+ if (refreshToken.revoked) return { active: false };
2398
+ let sessionId = refreshToken.sessionId ?? void 0;
2399
+ if (sessionId) {
2400
+ const session = await ctx.context.adapter.findOne({
2401
+ model: "session",
2402
+ where: [{
2403
+ field: "id",
2404
+ value: sessionId
2405
+ }]
2406
+ });
2407
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) sessionId = void 0;
2408
+ }
2409
+ let user = void 0;
2410
+ if (refreshToken.userId) user = await ctx.context.internalAdapter.findUserById(refreshToken?.userId) ?? void 0;
2411
+ return {
2412
+ active: true,
2413
+ client_id: clientId,
2414
+ iss: ((opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options)?.jwt?.issuer ?? ctx.context.baseURL,
2415
+ sub: user?.id,
2416
+ sid: sessionId,
2417
+ exp: Math.floor(new Date(refreshToken.expiresAt).getTime() / 1e3),
2418
+ iat: Math.floor(new Date(refreshToken.createdAt).getTime() / 1e3),
2419
+ scope: refreshToken.scopes?.join(" "),
2420
+ token_type: confirmationTokenType(refreshToken.confirmation),
2421
+ ...refreshToken.confirmation ? { cnf: refreshToken.confirmation } : {}
2422
+ };
2423
+ }
2424
+ function createInvalidAccessTokenError() {
2425
+ return new APIError$1("UNAUTHORIZED", {
2426
+ error_description: INVALID_ACCESS_TOKEN_ERROR_DESCRIPTION,
2427
+ error: "invalid_token"
2428
+ }, { "WWW-Authenticate": INVALID_ACCESS_TOKEN_WWW_AUTHENTICATE });
2429
+ }
2430
+ function isInactiveTokenError(error) {
2431
+ return error.status === "BAD_REQUEST" || error.body?.error === "invalid_token";
2432
+ }
2433
+ /**
2434
+ * We don't know the access token format so we try to validate it as a JWT
2435
+ * first, then as an opaque token. Returns the RFC 7662 introspection payload
2436
+ * alongside the per-issuance UserInfo claims the opaque row persisted (empty for
2437
+ * a JWT access token, which resolves UserInfo claims by scope).
2438
+ */
2439
+ async function resolveAccessTokenValidation(ctx, opts, token, clientId) {
2440
+ try {
2441
+ return {
2442
+ payload: await validateJwtAccessToken(ctx, opts, token, clientId),
2443
+ requestedUserInfoClaims: []
2444
+ };
2445
+ } catch (err) {
2446
+ if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
2447
+ else throw new Error(err);
2448
+ }
2449
+ try {
2450
+ return await validateOpaqueAccessToken(ctx, opts, token, clientId);
2451
+ } catch (err) {
2452
+ if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
2453
+ else throw new Error("Unknown error validating access token");
2454
+ }
2455
+ throw createInvalidAccessTokenError();
2456
+ }
2457
+ /**
2458
+ * Validates an access token (JWT or opaque) and returns its RFC 7662
2459
+ * introspection-format payload.
2460
+ *
2461
+ * @internal
2462
+ */
2463
+ async function validateAccessToken(ctx, opts, token, clientId) {
2464
+ return (await resolveAccessTokenValidation(ctx, opts, token, clientId)).payload;
2465
+ }
2466
+ async function requireActiveAccessToken(ctx, opts, token, clientId) {
2467
+ const payload = await validateAccessToken(ctx, opts, token, clientId);
2468
+ if (payload.active) return payload;
2469
+ throw createInvalidAccessTokenError();
2470
+ }
2471
+ /**
2472
+ * UserInfo entry point: validates the access token, requires it active, and
2473
+ * returns the introspection payload together with the OIDC `claims.userinfo`
2474
+ * names persisted for the token. Opaque tokens carry the names on their row;
2475
+ * JWT access tokens return an empty list and resolve UserInfo claims by scope.
2476
+ */
2477
+ async function requireActiveAccessTokenWithClaims(ctx, opts, token, clientId) {
2478
+ const { payload, requestedUserInfoClaims } = await resolveAccessTokenValidation(ctx, opts, token, clientId);
2479
+ if (payload.active) return {
2480
+ payload,
2481
+ requestedUserInfoClaims
2482
+ };
2483
+ throw createInvalidAccessTokenError();
2484
+ }
2485
+ /**
2486
+ * Resolves pairwise sub on an introspection payload.
2487
+ * Applied at the presentation layer so internal validation functions
2488
+ * keep real user.id (needed for user lookup in /userinfo).
2489
+ */
2490
+ async function resolveIntrospectionSub(ctx, opts, payload, introspectingClient) {
2491
+ if (!payload.active || !payload.sub) return payload;
2492
+ const issuerClientId = payload.client_id ?? payload.azp;
2493
+ if (!issuerClientId) return payload;
2494
+ const issuingClient = issuerClientId === introspectingClient.clientId ? introspectingClient : await getClient(ctx, opts, issuerClientId);
2495
+ if (!issuingClient) return payload;
2496
+ const resolvedSub = await resolveSubjectIdentifier(payload.sub, issuingClient, opts);
2497
+ return {
2498
+ ...payload,
2499
+ sub: resolvedSub
2500
+ };
2501
+ }
2502
+ async function introspectEndpoint(ctx, opts) {
2503
+ let { token, token_type_hint } = ctx.body;
2504
+ if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
2505
+ const { clientId: client_id, clientSecret: client_secret, preVerified, authMethod } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
2506
+ if (!client_id || !client_secret && !preVerified) throw new APIError$1("UNAUTHORIZED", {
2507
+ error_description: "missing required credentials",
2508
+ error: "invalid_client"
2509
+ });
2510
+ if (token && typeof token === "string") token = stripAccessTokenAuthorizationScheme(token);
2511
+ if (!token?.length) throw new APIError$1("BAD_REQUEST", {
2512
+ error_description: "missing a required token for introspection",
2513
+ error: "invalid_request"
2514
+ });
2515
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, void 0, authMethod);
2516
+ try {
2517
+ if (token_type_hint === void 0 || token_type_hint === "access_token") try {
2518
+ return resolveIntrospectionSub(ctx, opts, await validateAccessToken(ctx, opts, token, client.clientId), client);
2519
+ } catch (error) {
2520
+ if (error instanceof APIError$1) {
2521
+ if (token_type_hint === "access_token") throw error;
2522
+ } else if (error instanceof Error) throw error;
2523
+ else throw new Error(error);
2524
+ }
2525
+ if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
2526
+ return resolveIntrospectionSub(ctx, opts, await validateRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId), client);
2527
+ } catch (error) {
2528
+ if (error instanceof APIError$1) {
2529
+ if (token_type_hint === "refresh_token") throw error;
2530
+ } else if (error instanceof Error) throw error;
2531
+ else throw new Error(error);
2532
+ }
2533
+ throw new APIError$1("BAD_REQUEST", {
2534
+ error_description: "token not found",
2535
+ error: "invalid_request"
2536
+ });
2537
+ } catch (error) {
2538
+ if (error instanceof APIError$1) {
2539
+ if (isInactiveTokenError(error)) return { active: false };
2540
+ throw error;
2541
+ } else if (error instanceof Error) {
2542
+ logger.error("Introspection error:", error.message, error.stack);
2543
+ throw new APIError$1("INTERNAL_SERVER_ERROR");
2544
+ } else {
2545
+ logger.error("Introspection error:", error);
2546
+ throw new APIError$1("INTERNAL_SERVER_ERROR");
2547
+ }
2548
+ }
2549
+ }
2550
+ //#endregion
2551
+ export { filterClaimsRequestUserInfoClaims as A, resolveResourcePolicy as C, getSupportedClaims as D, STANDARD_CLAIM_NAMES as E, isValidOidcClaimsRequest as M, canSatisfyEssentialAcrRequest as O, logEnforcePerClientResourcesResolution as S, STANDARD_CLAIMS as T, assertIdentifierValid as _, invalidateRefreshFamily as a, invalidateResourceCache as b, ResourceUriSchema as c, clientJwksSchema as d, clientRegistrationRequestSchema as f, JWS_ALGORITHMS as g, validateClientCredentialsScopes as h, getOAuthProviderApi as i, getRequestedUserInfoClaims as j, claimsRequestParameterSchema as k, SafeUrlSchema as l, normalizeClientCredentialsScopes as m, introspect_exports as n, tokenEndpoint as o, oauthClientMetadataSchema as p, decodeRefreshToken as r, userInfoEndpoint as s, introspectEndpoint as t, authorizationQuerySchema as u, extractRepeatedResourceFromForm as v, seedResources as w, isAudienceClaimAllowed as x, getResource as y };