@better-auth/oauth-provider 1.7.0-beta.5 → 1.7.0-beta.6

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