@better-auth/oauth-provider 1.7.0-beta.1 → 1.7.0-beta.10

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