@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.
package/dist/index.mjs CHANGED
@@ -1,23 +1,24 @@
1
- import { n as isPrivateHostname } from "./client-assertion-DmT1B6_6.mjs";
2
- import { n as mcpHandler } from "./mcp-CYnz-MXn.mjs";
3
- import { A as verifyOAuthQueryParams, C as signedQueryIssuedAtParam, D as toClientDiscoveryArray, E as toAudienceClaim, O as toResourceList, S as searchParamsToQuery, T as storeToken, _ as postLoginClearedParam, a as extractClientCredentials, b as resolveSessionAuthTime, d as isPKCERequired, f as isSessionFreshForSignedQuery, g as parsePrompt, h as parseClientMetadata, i as destructureCredentials, k as validateClientCredentials, l as getSignedQueryIssuedAt, m as normalizeTimestampValue, n as clientAllowsGrant, o as getClient, p as mergeDiscoveryMetadata, r as decryptStoredClientSecret, s as getJwtPlugin, t as checkResource, u as getStoredToken, v as removeMaxAgeFromQuery, w as storeClientSecret, x as resolveSubjectIdentifier, y as removePromptFromQuery } from "./utils-D2dLqo7f.mjs";
4
- import { t as PACKAGE_VERSION } from "./version-B1ZiRmxj.mjs";
5
- import { APIError, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, dispatchAuthEndpoint, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
6
- import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
7
- import { APIError as APIError$1 } from "better-call";
8
- import { PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
1
+ import { D as applyOAuthProviderMetadataExtensions, E as verifyOAuthQueryParams, F as getSupportedGrantTypes, L as isExtensionTokenEndpointAuthMethod, M as getClientDiscoveries, P as getSupportedAuthMethods, R as validateOAuthProviderExtensions, S as storeToken, T as validateClientCredentials, _ as removePromptFromQuery, a as getClient, b as searchParamsToQuery, c as getStoredToken, d as mergeDiscoveryMetadata, g as removeMaxAgeFromQuery, h as parsePrompt, i as extractClientCredentials, j as extendOAuthProvider, l as isPKCERequired, m as parseClientMetadata, n as decryptStoredClientSecret, o as getJwtPlugin, p as parseBearerToken, r as destructureCredentials, t as clientAllowsGrant, u as isSessionFreshForSignedQuery, w as toResourceList, x as storeClientSecret, y as resolveSubjectIdentifier } from "./utils-Baq6atYN.mjs";
2
+ import { a as setSignedOAuthQueryParameterNames, i as postLoginClearedParam, n as canonicalizeOAuthQueryParams, o as signedQueryIssuedAtParam, r as getSignedQueryIssuedAt } from "./signed-query-CFv2jNMT.mjs";
3
+ import { _ as invalidateResourceCache, a as invalidateRefreshFamily, b as resolveResourcePolicy, c as ResourceUriSchema, d as clientRegistrationRequestSchema, f as JWS_ALGORITHMS, g as getResource, h as extractRepeatedResourceFromForm, i as getOAuthProviderApi, l as SafeUrlSchema, m as buildClientResourceLinkId, o as tokenEndpoint, p as assertIdentifierValid, r as decodeRefreshToken, s as userInfoEndpoint, t as introspectEndpoint, u as authorizationQuerySchema, v as isAudienceClaimAllowed, x as seedResources, y as logEnforcePerClientResourcesResolution } from "./introspect-BXqKFUQZ.mjs";
4
+ import { n as consumeClientAssertion, r as isPrivateHostname } from "./client-assertion-CctbJywV.mjs";
5
+ import { t as PACKAGE_VERSION } from "./version-CUu3vBtU.mjs";
6
+ import { t as raiseResourceServerChallenge } from "./resource-challenge-B-cqv4ur.mjs";
9
7
  import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
10
8
  import { isLoopbackHost, isLoopbackIP } from "@better-auth/core/utils/host";
9
+ import { APIError, NO_STORE_HEADERS, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, dispatchAuthEndpoint, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
11
10
  import { generateRandomString, makeSignature } from "better-auth/crypto";
12
- import { defineRequestState } from "@better-auth/core/context";
11
+ import { APIError as APIError$1 } from "better-call";
12
+ import { defineRequestState, runWithTransaction } from "@better-auth/core/context";
13
13
  import { logger } from "@better-auth/core/env";
14
14
  import { BetterAuthError } from "@better-auth/core/error";
15
15
  import { parseSetCookieHeader } from "better-auth/cookies";
16
16
  import { mergeSchema } from "better-auth/db";
17
17
  import * as z from "zod";
18
+ import { DPOP_SIGNING_ALGORITHMS, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
19
+ import { getJwks, stripAccessTokenAuthorizationScheme } from "better-auth/oauth2";
20
+ import { compactVerify, createLocalJWKSet, decodeJwt, jwtVerify } from "jose";
18
21
  import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
19
- import { SignJWT, base64url, compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader } from "jose";
20
- import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
21
22
  //#region src/consent.ts
22
23
  async function consentEndpoint(ctx, opts, authorize) {
23
24
  const oauthRequest = await oAuthState.get();
@@ -159,1019 +160,6 @@ async function postLogin(ctx, authorize) {
159
160
  return await authorize(ctx, { postLogin: state?.postLoginClearedForSession !== void 0 && state.postLoginClearedForSession === session?.session.id });
160
161
  }
161
162
  //#endregion
162
- //#region src/types/zod.ts
163
- const DANGEROUS_SCHEMES = [
164
- "javascript:",
165
- "data:",
166
- "vbscript:"
167
- ];
168
- /**
169
- * Validates an RFC 8707 resource indicator. The value must be an absolute URI
170
- * with no fragment (RFC 8707 §2). Unlike a redirect URI it is not restricted to
171
- * HTTPS, because a resource server identifier may use any absolute URI scheme;
172
- * the configured `validAudiences` allowlist is the authoritative control over
173
- * which resources a token may target.
174
- */
175
- const ResourceUriSchema = z.string().superRefine((val, ctx) => {
176
- if (!URL.canParse(val)) {
177
- ctx.addIssue({
178
- code: "custom",
179
- message: "resource must be an absolute URI",
180
- fatal: true
181
- });
182
- return z.NEVER;
183
- }
184
- if (val.includes("#")) {
185
- ctx.addIssue({
186
- code: "custom",
187
- message: "resource must not contain a fragment"
188
- });
189
- return;
190
- }
191
- if (DANGEROUS_SCHEMES.includes(new URL(val).protocol)) ctx.addIssue({
192
- code: "custom",
193
- message: "resource cannot use javascript:, data:, or vbscript: scheme"
194
- });
195
- });
196
- const authorizationPromptTokenSchema = z.enum([
197
- "none",
198
- "consent",
199
- "login",
200
- "create",
201
- "select_account"
202
- ]);
203
- const authorizationPromptSchema = z.string().superRefine((value, ctx) => {
204
- const promptTokens = value.split(" ").map((token) => token.trim()).filter(Boolean);
205
- const promptSet = /* @__PURE__ */ new Set();
206
- if (!promptTokens.length) {
207
- ctx.addIssue({
208
- code: "custom",
209
- message: "prompt must include at least one value"
210
- });
211
- return;
212
- }
213
- for (const token of promptTokens) {
214
- const result = authorizationPromptTokenSchema.safeParse(token);
215
- if (!result.success) {
216
- ctx.addIssue({
217
- code: "custom",
218
- message: `unsupported prompt value: ${token}`
219
- });
220
- continue;
221
- }
222
- promptSet.add(result.data);
223
- }
224
- if (promptSet.has("none") && promptSet.size > 1) ctx.addIssue({
225
- code: "custom",
226
- message: "prompt=none cannot be combined with other prompt values"
227
- });
228
- });
229
- const maxAgeSchema = z.union([z.number(), z.string().trim().min(1)]).transform((value, ctx) => {
230
- const maxAge = typeof value === "number" ? value : Number(value);
231
- if (!Number.isInteger(maxAge) || maxAge < 0) {
232
- ctx.addIssue({
233
- code: "custom",
234
- message: "max_age must be a non-negative integer"
235
- });
236
- return z.NEVER;
237
- }
238
- return maxAge;
239
- });
240
- /**
241
- * Runtime schema for OAuthAuthorizationQuery.
242
- * Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
243
- */
244
- const authorizationQuerySchema = z.object({
245
- response_type: z.string().pipe(z.enum(["code"])).optional(),
246
- request_uri: z.string().optional(),
247
- redirect_uri: SafeUrlSchema.optional(),
248
- scope: z.string().optional(),
249
- state: z.string().optional(),
250
- client_id: z.string(),
251
- prompt: authorizationPromptSchema.optional(),
252
- display: z.string().optional(),
253
- ui_locales: z.string().optional(),
254
- max_age: maxAgeSchema.optional(),
255
- acr_values: z.string().optional(),
256
- login_hint: z.string().optional(),
257
- id_token_hint: z.string().optional(),
258
- code_challenge: z.string().optional(),
259
- code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
260
- nonce: z.string().optional(),
261
- resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional()
262
- }).passthrough();
263
- const storedAuthorizationQuerySchema = authorizationQuerySchema.extend({ redirect_uri: SafeUrlSchema });
264
- /**
265
- * Runtime schema for the authorization code verification value.
266
- * Validates structure on deserialization from the JSON blob stored in the DB.
267
- * Uses passthrough so future fields (e.g. from authorization challenge) don't break parsing.
268
- */
269
- const verificationValueSchema = z.object({
270
- type: z.literal("authorization_code"),
271
- query: storedAuthorizationQuerySchema,
272
- sessionId: z.string(),
273
- userId: z.string(),
274
- referenceId: z.string().optional(),
275
- authTime: z.number().optional(),
276
- resource: z.array(z.string()).optional()
277
- }).passthrough();
278
- //#endregion
279
- //#region src/userinfo.ts
280
- /**
281
- * Provides shared /userinfo and id_token claims functionality
282
- *
283
- * @see https://openid.net/specs/openid-connect-core-1_0.html#NormalClaims
284
- */
285
- function userNormalClaims(user, scopes) {
286
- const name = user.name.split(" ").filter((v) => v !== "");
287
- const profile = {
288
- name: user.name ?? void 0,
289
- picture: user.image ?? void 0,
290
- given_name: name.length > 1 ? name.slice(0, -1).join(" ") : void 0,
291
- family_name: name.length > 1 ? name.at(-1) : void 0
292
- };
293
- const email = {
294
- email: user.email ?? void 0,
295
- email_verified: user.emailVerified ?? false
296
- };
297
- return {
298
- sub: user.id ?? void 0,
299
- ...scopes.includes("profile") ? profile : {},
300
- ...scopes.includes("email") ? email : {}
301
- };
302
- }
303
- /**
304
- * Handles the /oauth2/userinfo endpoint
305
- */
306
- async function userInfoEndpoint(ctx, opts) {
307
- const authorization = ctx.headers?.get("authorization");
308
- const token = typeof authorization === "string" && authorization?.startsWith("Bearer ") ? authorization?.replace("Bearer ", "") : authorization;
309
- if (!token?.length) throw new APIError("UNAUTHORIZED", {
310
- error_description: "authorization header not found",
311
- error: "invalid_request"
312
- });
313
- const jwt = await validateAccessToken(ctx, opts, token);
314
- if (!jwt.active) throw new APIError("UNAUTHORIZED", {
315
- error_description: "the access token is invalid or has been revoked",
316
- error: "invalid_token"
317
- });
318
- const scopes = jwt.scope?.split(" ");
319
- if (!scopes?.includes("openid")) throw new APIError("BAD_REQUEST", {
320
- error_description: "Missing required scope",
321
- error: "invalid_scope"
322
- });
323
- if (!jwt.sub) throw new APIError("BAD_REQUEST", {
324
- error_description: "user not found",
325
- error: "invalid_request"
326
- });
327
- const user = await ctx.context.internalAdapter.findUserById(jwt.sub);
328
- if (!user) throw new APIError("BAD_REQUEST", {
329
- error_description: "user not found",
330
- error: "invalid_request"
331
- });
332
- const baseUserClaims = userNormalClaims(user, scopes ?? []);
333
- if (opts.pairwiseSecret) {
334
- const clientId = jwt.client_id ?? jwt.azp;
335
- if (clientId) {
336
- const client = await getClient(ctx, opts, clientId);
337
- if (client) baseUserClaims.sub = await resolveSubjectIdentifier(user.id, client, opts);
338
- }
339
- }
340
- const additionalInfoUserClaims = opts.customUserInfoClaims && scopes?.length ? await opts.customUserInfoClaims({
341
- user,
342
- scopes,
343
- jwt
344
- }) : {};
345
- return {
346
- ...baseUserClaims,
347
- ...additionalInfoUserClaims
348
- };
349
- }
350
- //#endregion
351
- //#region src/token.ts
352
- /**
353
- * Handles the /oauth2/token endpoint by delegating
354
- * the grant types
355
- */
356
- async function tokenEndpoint(ctx, opts) {
357
- const grantType = ctx.body.grant_type;
358
- if (opts.grantTypes && !opts.grantTypes.includes(grantType)) throw new APIError("BAD_REQUEST", {
359
- error_description: `unsupported grant_type ${grantType}`,
360
- error: "unsupported_grant_type"
361
- });
362
- switch (grantType) {
363
- case "authorization_code": return handleAuthorizationCodeGrant(ctx, opts);
364
- case "client_credentials": return handleClientCredentialsGrant(ctx, opts);
365
- case "refresh_token": return handleRefreshTokenGrant(ctx, opts);
366
- }
367
- }
368
- async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, resources, referenceId, overrides) {
369
- const iat = overrides?.iat ?? Math.floor(Date.now() / 1e3);
370
- const exp = overrides?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
371
- const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
372
- user,
373
- scopes,
374
- resources,
375
- referenceId,
376
- metadata: parseClientMetadata(client.metadata)
377
- }) : {};
378
- const jwtPluginOptions = getJwtPlugin(ctx.context).options;
379
- return signJWT(ctx, {
380
- options: jwtPluginOptions,
381
- payload: {
382
- ...customClaims,
383
- sub: user?.id,
384
- aud: toAudienceClaim(audience),
385
- azp: client.clientId,
386
- scope: scopes.join(" "),
387
- sid: overrides?.sid,
388
- iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
389
- iat,
390
- exp
391
- }
392
- });
393
- }
394
- /**
395
- * Computes an OIDC hash (at_hash, c_hash) per OIDC Core §3.1.3.6.
396
- * Hashes the token, takes the left half, and base64url-encodes it.
397
- */
398
- async function computeOidcHash(token, signingAlg) {
399
- let hashAlg;
400
- if (signingAlg === "EdDSA") hashAlg = "SHA-512";
401
- else if (signingAlg.endsWith("384")) hashAlg = "SHA-384";
402
- else if (signingAlg.endsWith("512")) hashAlg = "SHA-512";
403
- else hashAlg = "SHA-256";
404
- const digest = new Uint8Array(await crypto.subtle.digest(hashAlg, new TextEncoder().encode(token)));
405
- return base64url.encode(digest.slice(0, digest.length / 2));
406
- }
407
- /**
408
- * Creates a user id token in code_authorization with scope of 'openid'
409
- * and hybrid/implicit (not yet implemented) flows
410
- */
411
- async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) {
412
- const iat = Math.floor(Date.now() / 1e3);
413
- const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
414
- const userClaims = userNormalClaims(user, scopes);
415
- const resolvedSub = await resolveSubjectIdentifier(user.id, client, opts);
416
- const authTimeSec = authTime != null ? Math.floor(authTime.getTime() / 1e3) : void 0;
417
- const acr = "urn:mace:incommon:iap:bronze";
418
- const customClaims = opts.customIdTokenClaims ? await opts.customIdTokenClaims({
419
- user,
420
- scopes,
421
- metadata: parseClientMetadata(client.metadata)
422
- }) : {};
423
- const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
424
- const resolvedKey = !opts.disableJwtPlugin && !jwtPluginOptions?.jwt?.sign ? await resolveSigningKey(ctx, jwtPluginOptions) : void 0;
425
- const signingAlg = opts.disableJwtPlugin ? "HS256" : resolvedKey?.alg ?? jwtPluginOptions?.jwks?.keyPairConfig?.alg;
426
- const atHash = accessToken ? await computeOidcHash(accessToken, signingAlg) : void 0;
427
- const emitSid = Boolean(client.enableEndSession || client.backchannelLogoutUri);
428
- const payload = {
429
- ...userClaims,
430
- auth_time: authTimeSec,
431
- acr,
432
- ...customClaims,
433
- at_hash: atHash,
434
- iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
435
- sub: resolvedSub,
436
- aud: client.clientId,
437
- nonce,
438
- iat,
439
- exp,
440
- sid: emitSid ? sessionId : void 0
441
- };
442
- if (opts.disableJwtPlugin && !client.clientSecret) return;
443
- 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, {
444
- options: jwtPluginOptions,
445
- payload,
446
- resolvedKey: resolvedKey ?? void 0
447
- });
448
- if (idToken && atHash && jwtPluginOptions?.jwt?.sign) {
449
- const header = decodeProtectedHeader(idToken);
450
- if (header.alg !== signingAlg) throw new APIError("INTERNAL_SERVER_ERROR", {
451
- 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.`,
452
- error: "server_error"
453
- });
454
- }
455
- return idToken;
456
- }
457
- /**
458
- * Encodes a refresh token for a client
459
- */
460
- async function encodeRefreshToken(opts, token, sessionId) {
461
- return (opts.prefix?.refreshToken ?? "") + (opts.formatRefreshToken?.encrypt ? opts.formatRefreshToken.encrypt(token, sessionId) : token);
462
- }
463
- /**
464
- * Decodes a refresh token for a client
465
- *
466
- * @internal
467
- */
468
- async function decodeRefreshToken(opts, token) {
469
- if (opts.prefix?.refreshToken) if (token.startsWith(opts.prefix.refreshToken)) token = token.replace(opts.prefix.refreshToken, "");
470
- else throw new APIError("BAD_REQUEST", {
471
- error_description: "refresh token not found",
472
- error: "invalid_token"
473
- });
474
- return opts.formatRefreshToken?.decrypt ? opts.formatRefreshToken?.decrypt(token) : { token };
475
- }
476
- async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, refreshId) {
477
- const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
478
- const exp = payload?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
479
- const token = opts.generateOpaqueAccessToken ? await opts.generateOpaqueAccessToken() : generateRandomString(32, "A-Z", "a-z");
480
- await ctx.context.adapter.create({
481
- model: "oauthAccessToken",
482
- data: {
483
- token: await storeToken(opts.storeTokens, token, "access_token"),
484
- clientId: client.clientId,
485
- sessionId: payload?.sid,
486
- userId: user?.id,
487
- referenceId,
488
- resources,
489
- refreshId,
490
- scopes,
491
- createdAt: /* @__PURE__ */ new Date(iat * 1e3),
492
- expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
493
- }
494
- });
495
- return (opts.prefix?.opaqueAccessToken ?? "") + token;
496
- }
497
- /**
498
- * Tear down the entire refresh-token family for a (client, user) pair, plus
499
- * any access tokens that reference those refresh rows, per RFC 9700 §4.14.
500
- * Access tokens are deleted first so the parent rows' foreign-key children
501
- * do not block the refresh-row delete.
502
- *
503
- * TODO(invalidate-family-race): the two `deleteMany` calls are not atomic
504
- * with respect to each other. Between them, a concurrent rotation in a
505
- * different worker can `create` a fresh refresh row (and, immediately after,
506
- * an access-token row referencing it) for the same (client, user) pair,
507
- * leaving the family partially rebuilt and the new refresh row orphaned of
508
- * any deletion. Closing this window requires the same transactional adapter
509
- * contract tracked under FIXME(strict-family-invalidation) in
510
- * `createRefreshToken`.
511
- *
512
- * @internal
513
- */
514
- async function invalidateRefreshFamily(ctx, clientId, userId) {
515
- const refreshTokens = await ctx.context.adapter.findMany({
516
- model: "oauthRefreshToken",
517
- where: [{
518
- field: "clientId",
519
- value: clientId
520
- }, {
521
- field: "userId",
522
- value: userId
523
- }]
524
- });
525
- if (refreshTokens.length) await ctx.context.adapter.deleteMany({
526
- model: "oauthAccessToken",
527
- where: [{
528
- field: "refreshId",
529
- operator: "in",
530
- value: refreshTokens.map((r) => r.id)
531
- }]
532
- });
533
- await ctx.context.adapter.deleteMany({
534
- model: "oauthRefreshToken",
535
- where: [{
536
- field: "clientId",
537
- value: clientId
538
- }, {
539
- field: "userId",
540
- value: userId
541
- }]
542
- });
543
- }
544
- async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, payload, originalRefresh, authTime, resources) {
545
- const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
546
- const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
547
- const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
548
- const sessionId = payload?.sid;
549
- const newRow = {
550
- token: await storeToken(opts.storeTokens, token, "refresh_token"),
551
- clientId: client.clientId,
552
- sessionId,
553
- userId: user.id,
554
- referenceId,
555
- authTime,
556
- scopes,
557
- resources,
558
- createdAt: /* @__PURE__ */ new Date(iat * 1e3),
559
- expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
560
- };
561
- if (!originalRefresh?.id) return {
562
- id: (await ctx.context.adapter.create({
563
- model: "oauthRefreshToken",
564
- data: newRow
565
- })).id,
566
- token: await encodeRefreshToken(opts, token, sessionId)
567
- };
568
- if (!await ctx.context.adapter.update({
569
- model: "oauthRefreshToken",
570
- where: [{
571
- field: "id",
572
- value: originalRefresh.id
573
- }, {
574
- field: "revoked",
575
- operator: "eq",
576
- value: null
577
- }],
578
- update: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
579
- })) throw new APIError("BAD_REQUEST", {
580
- error_description: "invalid refresh token",
581
- error: "invalid_grant"
582
- });
583
- return {
584
- id: (await ctx.context.adapter.create({
585
- model: "oauthRefreshToken",
586
- data: newRow
587
- })).id,
588
- token: await encodeRefreshToken(opts, token, sessionId)
589
- };
590
- }
591
- async function createUserTokens(ctx, opts, params) {
592
- const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, verificationValue } = params;
593
- const iat = Math.floor(Date.now() / 1e3);
594
- const defaultExp = iat + (user ? opts.accessTokenExpiresIn ?? 3600 : opts.m2mAccessTokenExpiresIn ?? 3600);
595
- const exp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
596
- return prev < curr ? prev : curr;
597
- }, defaultExp) : defaultExp;
598
- const resourceResult = await checkResource(ctx, opts, params?.resources, scopes);
599
- if (!resourceResult.success) throw new APIError("BAD_REQUEST", {
600
- error_description: "requested resource invalid",
601
- error: "invalid_target"
602
- });
603
- const audience = resourceResult.audience;
604
- const isRefreshToken = user && clientAllowsGrant(client, "refresh_token") && (existingRefreshToken?.scopes?.includes("offline_access") || scopes.includes("offline_access"));
605
- const isJwtAccessToken = audience && !opts.disableJwtPlugin;
606
- const isIdToken = user && scopes.includes("openid");
607
- const customFields = opts.customTokenResponseFields ? await opts.customTokenResponseFields({
608
- grantType,
609
- user,
610
- scopes,
611
- metadata: parseClientMetadata(client.metadata),
612
- verificationValue
613
- }) : void 0;
614
- const refreshResources = params?.refreshToken?.resources ?? params?.originalResources ?? params?.resources;
615
- const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
616
- iat,
617
- exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
618
- sid: sessionId
619
- }, existingRefreshToken, authTime, refreshResources) : void 0;
620
- const [accessToken, refreshToken] = await Promise.all([isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, params?.resources, referenceId, {
621
- iat,
622
- exp,
623
- sid: sessionId
624
- }) : createOpaqueAccessToken(ctx, opts, user, client, scopes, {
625
- iat,
626
- exp,
627
- sid: sessionId
628
- }, params?.resources, referenceId, earlyRefreshToken?.id), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
629
- iat,
630
- exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
631
- sid: sessionId
632
- }, existingRefreshToken, authTime, refreshResources) : void 0]);
633
- const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) : void 0;
634
- return ctx.json({
635
- ...customFields,
636
- access_token: accessToken,
637
- expires_in: exp - iat,
638
- expires_at: exp,
639
- token_type: "Bearer",
640
- refresh_token: refreshToken?.token,
641
- scope: scopes.join(" "),
642
- id_token: idToken
643
- }, { headers: {
644
- "Cache-Control": "no-store",
645
- Pragma: "no-cache"
646
- } });
647
- }
648
- /** Checks verification value */
649
- async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resource) {
650
- const verification = await ctx.context.internalAdapter.consumeVerificationValue(await storeToken(opts.storeTokens, code, "authorization_code"));
651
- if (!verification) throw new APIError("UNAUTHORIZED", {
652
- error_description: "invalid code",
653
- error: "invalid_grant"
654
- });
655
- let rawValue;
656
- try {
657
- rawValue = JSON.parse(verification.value);
658
- } catch {
659
- throw new APIError("UNAUTHORIZED", {
660
- error_description: "malformed verification value",
661
- error: "invalid_grant"
662
- });
663
- }
664
- const parsed = verificationValueSchema.safeParse(rawValue);
665
- if (!parsed.success) throw new APIError("UNAUTHORIZED", {
666
- error_description: "malformed verification value",
667
- error: "invalid_grant"
668
- });
669
- const verificationValue = parsed.data;
670
- if (verificationValue.query.client_id !== client_id) throw new APIError("UNAUTHORIZED", {
671
- error_description: "invalid client_id",
672
- error: "invalid_client"
673
- });
674
- if (verificationValue.query?.redirect_uri && verificationValue.query?.redirect_uri !== redirect_uri) throw new APIError("BAD_REQUEST", {
675
- error_description: "redirect_uri mismatch",
676
- error: "invalid_request"
677
- });
678
- const storedResources = toResourceList(verificationValue.resource) ?? toResourceList(verificationValue.query.resource);
679
- const effectiveResources = resource ?? storedResources;
680
- if (resource && storedResources) {
681
- const requestedSet = new Set(resource);
682
- const authorizedSet = new Set(storedResources);
683
- for (const r of requestedSet) if (!authorizedSet.has(r)) throw new APIError("BAD_REQUEST", {
684
- error_description: "requested resource not authorized",
685
- error: "invalid_target"
686
- });
687
- }
688
- return {
689
- verificationValue,
690
- effectiveResources,
691
- authorizedResources: storedResources
692
- };
693
- }
694
- /**
695
- * Obtains new Session Jwt and Refresh Tokens using a code
696
- */
697
- async function handleAuthorizationCodeGrant(ctx, opts) {
698
- const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
699
- const { code, code_verifier, redirect_uri, resource } = ctx.body;
700
- const resources = toResourceList(resource);
701
- if (!client_id) throw new APIError("BAD_REQUEST", {
702
- error_description: "client_id is required",
703
- error: "invalid_request"
704
- });
705
- if (!code) throw new APIError("BAD_REQUEST", {
706
- error_description: "code is required",
707
- error: "invalid_request"
708
- });
709
- if (!redirect_uri) throw new APIError("BAD_REQUEST", {
710
- error_description: "redirect_uri is required",
711
- error: "invalid_request"
712
- });
713
- const isAuthCodeWithSecret = client_id && client_secret;
714
- const isAuthCodeWithPkce = client_id && code && code_verifier;
715
- if (!isAuthCodeWithSecret && !isAuthCodeWithPkce && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
716
- error_description: "Either code_verifier or client_secret is required",
717
- error: "invalid_request"
718
- });
719
- /** Get and check Verification Value */
720
- const { verificationValue, effectiveResources, authorizedResources } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
721
- const scopes = verificationValue.query.scope?.split(" ");
722
- if (!scopes) throw new APIError("INTERNAL_SERVER_ERROR", {
723
- error_description: "verification scope unset",
724
- error: "invalid_scope"
725
- });
726
- /** Verify Client */
727
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerifiedClient, "authorization_code");
728
- if (isPKCERequired(client, (verificationValue.query?.scope)?.split(" ") || [])) {
729
- if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
730
- error_description: "PKCE is required for this client",
731
- error: "invalid_request"
732
- });
733
- } else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret || preVerifiedClient)) throw new APIError("BAD_REQUEST", {
734
- error_description: "Either PKCE (code_verifier) or client authentication (client_secret or client_assertion) is required",
735
- error: "invalid_request"
736
- });
737
- /** Check PKCE challenge if verifier is provided */
738
- const pkceUsedInAuth = !!verificationValue.query?.code_challenge;
739
- const pkceUsedInToken = !!code_verifier;
740
- if (pkceUsedInAuth || pkceUsedInToken) {
741
- if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
742
- error_description: "code_verifier required because PKCE was used in authorization",
743
- error: "invalid_request"
744
- });
745
- if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
746
- error_description: "code_verifier provided but PKCE was not used in authorization",
747
- error: "invalid_request"
748
- });
749
- if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("UNAUTHORIZED", {
750
- error_description: "code verification failed",
751
- error: "invalid_request"
752
- });
753
- }
754
- /** Get user */
755
- if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
756
- error_description: "missing user, user may have been deleted",
757
- error: "invalid_user"
758
- });
759
- const user = await ctx.context.internalAdapter.findUserById(verificationValue.userId);
760
- if (!user) throw new APIError("BAD_REQUEST", {
761
- error_description: "missing user, user may have been deleted",
762
- error: "invalid_user"
763
- });
764
- const session = await ctx.context.adapter.findOne({
765
- model: "session",
766
- where: [{
767
- field: "id",
768
- value: verificationValue.sessionId
769
- }]
770
- });
771
- if (!session || session.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
772
- error_description: "session no longer exists",
773
- error: "invalid_request"
774
- });
775
- const authTime = verificationValue.authTime != null ? normalizeTimestampValue(verificationValue.authTime) : resolveSessionAuthTime(session);
776
- return createUserTokens(ctx, opts, {
777
- client,
778
- scopes: verificationValue.query.scope?.split(" ") ?? [],
779
- user,
780
- grantType: "authorization_code",
781
- referenceId: verificationValue.referenceId,
782
- sessionId: session.id,
783
- nonce: verificationValue.query?.nonce,
784
- authTime,
785
- verificationValue,
786
- resources: effectiveResources,
787
- originalResources: authorizedResources
788
- });
789
- }
790
- /**
791
- * Grant that allows direct access to an API using the application's credentials
792
- * This grant is for M2M so the concept of a user id does not exist on the token.
793
- *
794
- * MUST follow https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
795
- */
796
- async function handleClientCredentialsGrant(ctx, opts) {
797
- const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
798
- const { scope, resource } = ctx.body;
799
- const resources = toResourceList(resource);
800
- if (!client_id) throw new APIError("BAD_REQUEST", {
801
- error_description: "Missing required client_id",
802
- error: "invalid_grant"
803
- });
804
- if (!client_secret && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
805
- error_description: "Missing a required client_secret",
806
- error: "invalid_grant"
807
- });
808
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient, "client_credentials");
809
- let requestedScopes = scope?.split(" ");
810
- if (requestedScopes) {
811
- const validScopes = new Set(client.scopes ?? opts.scopes);
812
- const oidcScopes = new Set([
813
- "openid",
814
- "profile",
815
- "email",
816
- "offline_access"
817
- ]);
818
- const invalidScopes = requestedScopes.filter((scope) => {
819
- return !validScopes?.has(scope) || oidcScopes.has(scope);
820
- });
821
- if (invalidScopes.length) throw new APIError("BAD_REQUEST", {
822
- error_description: `The following scopes are invalid: ${invalidScopes.join(", ")}`,
823
- error: "invalid_scope"
824
- });
825
- }
826
- if (!requestedScopes) requestedScopes = client.scopes ?? opts.clientCredentialGrantDefaultScopes ?? opts.scopes ?? [];
827
- return createUserTokens(ctx, opts, {
828
- client,
829
- scopes: requestedScopes,
830
- grantType: "client_credentials",
831
- resources
832
- });
833
- }
834
- /**
835
- * Obtains new Session Jwt and Refresh Tokens using a refresh token
836
- *
837
- * Refresh tokens will only allow the same or lesser scopes as the initial authorize request.
838
- * To add scopes, you must restart the authorize process again.
839
- */
840
- async function handleRefreshTokenGrant(ctx, opts) {
841
- const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
842
- const { refresh_token, scope, resource } = ctx.body;
843
- const resources = toResourceList(resource);
844
- if (!client_id) throw new APIError("BAD_REQUEST", {
845
- error_description: "Missing required client_id",
846
- error: "invalid_grant"
847
- });
848
- if (!refresh_token) throw new APIError("BAD_REQUEST", {
849
- error_description: "Missing a required refresh_token for refresh_token grant",
850
- error: "invalid_grant"
851
- });
852
- const decodedRefresh = await decodeRefreshToken(opts, refresh_token);
853
- const refreshToken = await ctx.context.adapter.findOne({
854
- model: "oauthRefreshToken",
855
- where: [{
856
- field: "token",
857
- value: await getStoredToken(opts.storeTokens, decodedRefresh.token, "refresh_token")
858
- }]
859
- });
860
- if (!refreshToken) throw new APIError("BAD_REQUEST", {
861
- error_description: "session not found",
862
- error: "invalid_grant"
863
- });
864
- if (refreshToken.clientId !== client_id) throw new APIError("BAD_REQUEST", {
865
- error_description: "invalid client_id",
866
- error: "invalid_client"
867
- });
868
- if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
869
- error_description: "invalid refresh token",
870
- error: "invalid_grant"
871
- });
872
- if (refreshToken.revoked) {
873
- await invalidateRefreshFamily(ctx, client_id, refreshToken.userId);
874
- throw new APIError("BAD_REQUEST", {
875
- error_description: "invalid refresh token",
876
- error: "invalid_grant"
877
- });
878
- }
879
- if (resources && refreshToken.resources && !resources.every((v) => refreshToken.resources?.includes(v))) throw new APIError("BAD_REQUEST", {
880
- error_description: "requested resource invalid",
881
- error: "invalid_target"
882
- });
883
- const scopes = refreshToken?.scopes;
884
- const requestedScopes = scope?.split(" ");
885
- if (requestedScopes) {
886
- const validScopes = new Set(scopes);
887
- for (const requestedScope of requestedScopes) if (!validScopes.has(requestedScope)) throw new APIError("BAD_REQUEST", {
888
- error_description: `unable to issue scope ${requestedScope}`,
889
- error: "invalid_scope"
890
- });
891
- }
892
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes, preVerifiedClient, "refresh_token");
893
- const user = await ctx.context.internalAdapter.findUserById(refreshToken.userId);
894
- if (!user) throw new APIError("BAD_REQUEST", {
895
- error_description: "user not found",
896
- error: "invalid_request"
897
- });
898
- const authTime = refreshToken.authTime != null ? normalizeTimestampValue(refreshToken.authTime) : void 0;
899
- return createUserTokens(ctx, opts, {
900
- client,
901
- scopes: requestedScopes ?? scopes,
902
- user,
903
- grantType: "refresh_token",
904
- referenceId: refreshToken.referenceId,
905
- sessionId: refreshToken.sessionId,
906
- refreshToken,
907
- resources: resources ?? refreshToken.resources,
908
- authTime
909
- });
910
- }
911
- //#endregion
912
- //#region src/introspect.ts
913
- /**
914
- * IMPORTANT NOTES:
915
- * Introspection follows RFC7662
916
- * https://datatracker.ietf.org/doc/html/rfc7662
917
- * - APIError: Continue catches (returnable to client)
918
- * - Error: Should immediately stop catches (internal error)
919
- */
920
- /**
921
- * Validates a JWT access token against the configured JWKs.
922
- *
923
- * @returns RFC7662 introspection format
924
- */
925
- async function validateJwtAccessToken(ctx, opts, token, clientId) {
926
- const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
927
- const jwtPluginOptions = jwtPlugin?.options;
928
- let jwtPayload;
929
- try {
930
- jwtPayload = await verifyJwsAccessToken(token, {
931
- jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
932
- return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
933
- },
934
- verifyOptions: {
935
- audience: opts.validAudiences ?? ctx.context.baseURL,
936
- issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL
937
- }
938
- });
939
- } catch (error) {
940
- if (error instanceof Error) {
941
- if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
942
- error_description: "invalid JWT signature",
943
- error: "invalid_request"
944
- });
945
- else if (error.name === "JWTExpired") return { active: false };
946
- else if (error.name === "JWTInvalid") return { active: false };
947
- throw error;
948
- }
949
- throw new Error(error);
950
- }
951
- if (!jwtPayload.azp) return { active: false };
952
- const client = await getClient(ctx, opts, jwtPayload.azp);
953
- if (!client || client?.disabled) return { active: false };
954
- if (clientId && jwtPayload.azp !== clientId) return { active: false };
955
- const sessionId = jwtPayload.sid;
956
- if (sessionId) {
957
- const session = await ctx.context.adapter.findOne({
958
- model: "session",
959
- where: [{
960
- field: "id",
961
- value: sessionId
962
- }]
963
- });
964
- if (!session || session.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
965
- }
966
- jwtPayload.client_id = jwtPayload.azp;
967
- jwtPayload.active = true;
968
- return jwtPayload;
969
- }
970
- /**
971
- * Searches for an opaque access token in the database and validates it
972
- *
973
- * @returns RFC7662 introspection format
974
- */
975
- async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
976
- let tokenValue = token;
977
- if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
978
- else throw new APIError$1("BAD_REQUEST", {
979
- error_description: "opaque access token not found",
980
- error: "invalid_request"
981
- });
982
- const accessToken = await ctx.context.adapter.findOne({
983
- model: "oauthAccessToken",
984
- where: [{
985
- field: "token",
986
- value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
987
- }]
988
- });
989
- if (!accessToken) throw new APIError$1("BAD_REQUEST", {
990
- error_description: "opaque access token not found",
991
- error: "invalid_token"
992
- });
993
- if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
994
- if (accessToken.revoked) return { active: false };
995
- let client;
996
- if (accessToken.clientId) {
997
- client = await getClient(ctx, opts, accessToken.clientId);
998
- if (!client || client?.disabled) return { active: false };
999
- if (clientId && accessToken.clientId !== clientId) return { active: false };
1000
- }
1001
- const sessionId = accessToken.sessionId ?? void 0;
1002
- if (sessionId) {
1003
- const session = await ctx.context.adapter.findOne({
1004
- model: "session",
1005
- where: [{
1006
- field: "id",
1007
- value: sessionId
1008
- }]
1009
- });
1010
- if (!session || session.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
1011
- }
1012
- let user;
1013
- if (accessToken.userId) user = await ctx.context.internalAdapter.findUserById(accessToken?.userId);
1014
- const resources = Array.isArray(accessToken.resources) ? accessToken.resources : void 0;
1015
- const audience = resources ? [...resources] : void 0;
1016
- if (audience?.length && accessToken.scopes?.includes("openid")) {
1017
- const userInfoEndpoint = `${ctx.context.baseURL}/oauth2/userinfo`;
1018
- if (!audience.includes(userInfoEndpoint)) audience.push(userInfoEndpoint);
1019
- }
1020
- const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
1021
- user,
1022
- scopes: accessToken.scopes,
1023
- referenceId: accessToken?.referenceId,
1024
- resources,
1025
- metadata: parseClientMetadata(client?.metadata)
1026
- }) : {};
1027
- const jwtPluginOptions = (opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options;
1028
- return {
1029
- ...customClaims,
1030
- active: true,
1031
- iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
1032
- aud: toAudienceClaim(audience),
1033
- client_id: accessToken.clientId,
1034
- sub: user?.id,
1035
- sid: sessionId,
1036
- exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
1037
- iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
1038
- scope: accessToken.scopes?.join(" ")
1039
- };
1040
- }
1041
- /**
1042
- * Validates a refresh token in the session store.
1043
- *
1044
- * @returns payload in RFC7662 introspection format
1045
- */
1046
- async function validateRefreshToken(ctx, opts, token, clientId) {
1047
- const refreshToken = await ctx.context.adapter.findOne({
1048
- model: "oauthRefreshToken",
1049
- where: [{
1050
- field: "token",
1051
- value: await getStoredToken(opts.storeTokens, token, "refresh_token")
1052
- }]
1053
- });
1054
- if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
1055
- error_description: "token not found",
1056
- error: "invalid_token"
1057
- });
1058
- if (!refreshToken.clientId || refreshToken.clientId !== clientId) return { active: false };
1059
- if (!refreshToken.expiresAt || refreshToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
1060
- if (refreshToken.revoked) return { active: false };
1061
- let sessionId = refreshToken.sessionId ?? void 0;
1062
- if (sessionId) {
1063
- const session = await ctx.context.adapter.findOne({
1064
- model: "session",
1065
- where: [{
1066
- field: "id",
1067
- value: sessionId
1068
- }]
1069
- });
1070
- if (!session || session.expiresAt < /* @__PURE__ */ new Date()) sessionId = void 0;
1071
- }
1072
- let user = void 0;
1073
- if (refreshToken.userId) user = await ctx.context.internalAdapter.findUserById(refreshToken?.userId) ?? void 0;
1074
- return {
1075
- active: true,
1076
- client_id: clientId,
1077
- iss: ((opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options)?.jwt?.issuer ?? ctx.context.baseURL,
1078
- sub: user?.id,
1079
- sid: sessionId,
1080
- exp: Math.floor(new Date(refreshToken.expiresAt).getTime() / 1e3),
1081
- iat: Math.floor(new Date(refreshToken.createdAt).getTime() / 1e3),
1082
- scope: refreshToken.scopes?.join(" ")
1083
- };
1084
- }
1085
- /**
1086
- * We don't know the access token format so we try to validate it
1087
- * as a JWT first, then as an opaque token.
1088
- *
1089
- * @returns RFC7662 introspection format
1090
- *
1091
- * @internal
1092
- */
1093
- async function validateAccessToken(ctx, opts, token, clientId) {
1094
- try {
1095
- return await validateJwtAccessToken(ctx, opts, token, clientId);
1096
- } catch (err) {
1097
- if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
1098
- else throw new Error(err);
1099
- }
1100
- try {
1101
- return await validateOpaqueAccessToken(ctx, opts, token, clientId);
1102
- } catch (err) {
1103
- if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
1104
- else throw new Error("Unknown error validating access token");
1105
- }
1106
- throw new APIError$1("BAD_REQUEST", {
1107
- error_description: "Invalid access token",
1108
- error: "invalid_request"
1109
- });
1110
- }
1111
- /**
1112
- * Resolves pairwise sub on an introspection payload.
1113
- * Applied at the presentation layer so internal validation functions
1114
- * keep real user.id (needed for user lookup in /userinfo).
1115
- */
1116
- async function resolveIntrospectionSub(opts, payload, client) {
1117
- if (payload.active && payload.sub) {
1118
- const resolvedSub = await resolveSubjectIdentifier(payload.sub, client, opts);
1119
- return {
1120
- ...payload,
1121
- sub: resolvedSub
1122
- };
1123
- }
1124
- return payload;
1125
- }
1126
- async function introspectEndpoint(ctx, opts) {
1127
- let { token, token_type_hint } = ctx.body;
1128
- if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
1129
- const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
1130
- if (!client_id || !client_secret && !preVerifiedClient) throw new APIError$1("UNAUTHORIZED", {
1131
- error_description: "missing required credentials",
1132
- error: "invalid_client"
1133
- });
1134
- if (token && typeof token === "string" && token.startsWith("Bearer ")) token = token.replace("Bearer ", "");
1135
- if (!token?.length) throw new APIError$1("BAD_REQUEST", {
1136
- error_description: "missing a required token for introspection",
1137
- error: "invalid_request"
1138
- });
1139
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
1140
- try {
1141
- if (token_type_hint === void 0 || token_type_hint === "access_token") try {
1142
- return resolveIntrospectionSub(opts, await validateAccessToken(ctx, opts, token, client.clientId), client);
1143
- } catch (error) {
1144
- if (error instanceof APIError$1) {
1145
- if (token_type_hint === "access_token") throw error;
1146
- } else if (error instanceof Error) throw error;
1147
- else throw new Error(error);
1148
- }
1149
- if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
1150
- return resolveIntrospectionSub(opts, await validateRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId), client);
1151
- } catch (error) {
1152
- if (error instanceof APIError$1) {
1153
- if (token_type_hint === "refresh_token") throw error;
1154
- } else if (error instanceof Error) throw error;
1155
- else throw new Error(error);
1156
- }
1157
- throw new APIError$1("BAD_REQUEST", {
1158
- error_description: "token not found",
1159
- error: "invalid_request"
1160
- });
1161
- } catch (error) {
1162
- if (error instanceof APIError$1) {
1163
- if (error.name === "BAD_REQUEST") return { active: false };
1164
- throw error;
1165
- } else if (error instanceof Error) {
1166
- logger.error("Introspection error:", error.message, error.stack);
1167
- throw new APIError$1("INTERNAL_SERVER_ERROR");
1168
- } else {
1169
- logger.error("Introspection error:", error);
1170
- throw new APIError$1("INTERNAL_SERVER_ERROR");
1171
- }
1172
- }
1173
- }
1174
- //#endregion
1175
163
  //#region src/logout.ts
1176
164
  const BACKCHANNEL_LOGOUT_EVENT_URI = "http://schemas.openid.net/event/backchannel-logout";
1177
165
  const LOGOUT_TOKEN_JWT_TYP = "logout+jwt";
@@ -1440,6 +428,155 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
1440
428
  }
1441
429
  }
1442
430
  //#endregion
431
+ //#region src/metadata.ts
432
+ function authServerMetadata(ctx, opts, overrides) {
433
+ const baseURL = ctx.context.baseURL;
434
+ const backchannelSupported = !overrides?.jwt_disabled;
435
+ return {
436
+ scopes_supported: overrides?.scopes_supported,
437
+ issuer: validateIssuerUrl(opts?.jwt?.issuer ?? baseURL),
438
+ authorization_endpoint: `${baseURL}/oauth2/authorize`,
439
+ token_endpoint: `${baseURL}/oauth2/token`,
440
+ jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
441
+ registration_endpoint: overrides?.dynamic_client_registration_supported ? `${baseURL}/oauth2/register` : void 0,
442
+ introspection_endpoint: `${baseURL}/oauth2/introspect`,
443
+ revocation_endpoint: `${baseURL}/oauth2/revoke`,
444
+ response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
445
+ response_modes_supported: ["query"],
446
+ grant_types_supported: overrides?.grant_types_supported ?? [
447
+ "authorization_code",
448
+ "client_credentials",
449
+ "refresh_token"
450
+ ],
451
+ token_endpoint_auth_methods_supported: overrides?.token_endpoint_auth_methods_supported ?? [
452
+ ...overrides?.public_client_supported ? ["none"] : [],
453
+ "client_secret_basic",
454
+ "client_secret_post",
455
+ "private_key_jwt"
456
+ ],
457
+ token_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
458
+ introspection_endpoint_auth_methods_supported: overrides?.endpoint_auth_methods_supported ?? [
459
+ "client_secret_basic",
460
+ "client_secret_post",
461
+ "private_key_jwt"
462
+ ],
463
+ introspection_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
464
+ revocation_endpoint_auth_methods_supported: overrides?.endpoint_auth_methods_supported ?? [
465
+ "client_secret_basic",
466
+ "client_secret_post",
467
+ "private_key_jwt"
468
+ ],
469
+ revocation_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
470
+ code_challenge_methods_supported: ["S256"],
471
+ authorization_response_iss_parameter_supported: true,
472
+ dpop_signing_alg_values_supported: overrides?.dpop_signing_alg_values_supported ?? [...DPOP_SIGNING_ALGORITHMS],
473
+ backchannel_logout_supported: backchannelSupported,
474
+ backchannel_logout_session_supported: backchannelSupported
475
+ };
476
+ }
477
+ /**
478
+ * Builds the authorization-server metadata shared by the
479
+ * `/.well-known/oauth-authorization-server` and `/.well-known/openid-configuration`
480
+ * responses, plus the inputs both need to finish their own document.
481
+ */
482
+ function buildAuthServerMetadata(ctx, opts) {
483
+ const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
484
+ const clientDiscoveries = getClientDiscoveries(opts);
485
+ const publicClientSupported = opts.allowUnauthenticatedClientRegistration || clientDiscoveries.length > 0;
486
+ return {
487
+ jwtPluginOptions,
488
+ clientDiscoveries,
489
+ authMetadata: authServerMetadata(ctx, jwtPluginOptions, {
490
+ scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
491
+ dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
492
+ public_client_supported: publicClientSupported,
493
+ grant_types_supported: getSupportedGrantTypes(opts),
494
+ token_endpoint_auth_methods_supported: getSupportedAuthMethods(opts, { includeNone: publicClientSupported }),
495
+ endpoint_auth_methods_supported: getSupportedAuthMethods(opts),
496
+ jwt_disabled: opts.disableJwtPlugin,
497
+ dpop_signing_alg_values_supported: opts.dpop?.signingAlgorithms
498
+ })
499
+ };
500
+ }
501
+ function oauthAuthorizationServerMetadata(ctx, opts) {
502
+ const { clientDiscoveries, authMetadata } = buildAuthServerMetadata(ctx, opts);
503
+ return applyOAuthProviderMetadataExtensions(ctx, opts, "oauth-authorization-server", {
504
+ ...mergeDiscoveryMetadata(clientDiscoveries),
505
+ ...authMetadata
506
+ });
507
+ }
508
+ function oidcServerMetadata(ctx, opts) {
509
+ const baseURL = ctx.context.baseURL;
510
+ const { jwtPluginOptions, clientDiscoveries, authMetadata } = buildAuthServerMetadata(ctx, opts);
511
+ const metadata = {
512
+ ...authMetadata,
513
+ claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
514
+ userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
515
+ subject_types_supported: opts.pairwiseSecret ? ["public", "pairwise"] : ["public"],
516
+ id_token_signing_alg_values_supported: (() => {
517
+ if (opts.disableJwtPlugin) return ["HS256"];
518
+ const primary = jwtPluginOptions?.jwks?.keyPairConfig?.alg ?? "EdDSA";
519
+ const extras = jwtPluginOptions?.jwks?.keyPairConfigs?.map((c) => c.alg) ?? [];
520
+ return Array.from(new Set([primary, ...extras]));
521
+ })(),
522
+ end_session_endpoint: `${baseURL}/oauth2/end-session`,
523
+ acr_values_supported: ["urn:mace:incommon:iap:bronze"],
524
+ prompt_values_supported: [
525
+ "login",
526
+ "consent",
527
+ "create",
528
+ "select_account",
529
+ "none"
530
+ ]
531
+ };
532
+ return applyOAuthProviderMetadataExtensions(ctx, opts, "openid-configuration", {
533
+ ...mergeDiscoveryMetadata(clientDiscoveries),
534
+ ...metadata
535
+ });
536
+ }
537
+ const METADATA_CACHE_CONTROL = "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400";
538
+ function metadataResponse(body, extraHeaders) {
539
+ const headers = new Headers(extraHeaders);
540
+ if (!headers.has("Cache-Control")) headers.set("Cache-Control", METADATA_CACHE_CONTROL);
541
+ headers.set("Content-Type", "application/json");
542
+ return new Response(JSON.stringify(body), {
543
+ status: 200,
544
+ headers
545
+ });
546
+ }
547
+ /**
548
+ * Provides an exportable `/.well-known/oauth-authorization-server`.
549
+ *
550
+ * Useful when basePath prevents the endpoint from being located at the root
551
+ * and must be provided manually.
552
+ *
553
+ * @external
554
+ */
555
+ const oauthProviderAuthServerMetadata = (auth, opts) => {
556
+ return async (request) => {
557
+ return metadataResponse(await auth.api.getOAuthServerConfig({
558
+ request,
559
+ asResponse: false
560
+ }), opts?.headers);
561
+ };
562
+ };
563
+ /**
564
+ * Provides an exportable `/.well-known/openid-configuration`.
565
+ *
566
+ * Useful when basePath prevents the endpoint from being located at the root
567
+ * and must be provided manually.
568
+ *
569
+ * @external
570
+ */
571
+ const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
572
+ return async (request) => {
573
+ return metadataResponse(await auth.api.getOpenIdConfig({
574
+ request,
575
+ asResponse: false
576
+ }), opts?.headers);
577
+ };
578
+ };
579
+ //#endregion
1443
580
  //#region src/oauth-endpoint.ts
1444
581
  /**
1445
582
  * Wraps `createAuthEndpoint` so zod schemas stay the single source of truth
@@ -1572,6 +709,64 @@ async function assertClientPrivileges(ctx, session, opts, action) {
1572
709
  })) throw new APIError("UNAUTHORIZED");
1573
710
  }
1574
711
  //#endregion
712
+ //#region src/utils/initial-access-token.ts
713
+ /**
714
+ * Builds an RFC 6750 §3 Bearer challenge for the registration endpoint. The
715
+ * `error` code maps to the status per RFC 6750 §3.1 (`invalid_request` → 400,
716
+ * `invalid_token` → 401) and is echoed in both the `WWW-Authenticate` challenge
717
+ * and the JSON body.
718
+ *
719
+ * @see https://datatracker.ietf.org/doc/html/rfc6750#section-3
720
+ */
721
+ function registrationBearerError(status, error, errorDescription) {
722
+ return new APIError$1(status, {
723
+ error,
724
+ error_description: errorDescription
725
+ }, {
726
+ "WWW-Authenticate": `Bearer error="${error}"`,
727
+ ...NO_STORE_HEADERS
728
+ });
729
+ }
730
+ /**
731
+ * Resolves an RFC 7591 initial access token from the request and authorizes it
732
+ * against the deployment-supplied validator.
733
+ *
734
+ * Returns the authorization (optionally carrying a `referenceId`) when a valid
735
+ * token is present, or `undefined` when no Bearer credentials were sent so the
736
+ * caller can fall back to session or open registration. Throws an RFC 6750
737
+ * Bearer challenge when the header is malformed, no validator is configured, or
738
+ * the token is rejected.
739
+ *
740
+ * @see https://datatracker.ietf.org/doc/html/rfc7591#section-3
741
+ */
742
+ async function authorizeInitialAccessToken(ctx, opts, clientMetadata) {
743
+ const headers = ctx.headers;
744
+ let initialAccessToken;
745
+ try {
746
+ initialAccessToken = parseBearerToken(headers?.get("authorization"));
747
+ } catch {
748
+ throw registrationBearerError("BAD_REQUEST", "invalid_request", "Malformed initial access token Authorization header");
749
+ }
750
+ if (!initialAccessToken || !headers) return;
751
+ if (!opts.validateInitialAccessToken) throw registrationBearerError("UNAUTHORIZED", "invalid_token", "Invalid initial access token");
752
+ let authorization;
753
+ try {
754
+ authorization = await opts.validateInitialAccessToken({
755
+ initialAccessToken,
756
+ headers,
757
+ clientMetadata
758
+ });
759
+ } catch (error) {
760
+ if (error instanceof APIError$1) throw error;
761
+ throw new APIError$1("INTERNAL_SERVER_ERROR", {
762
+ error: "server_error",
763
+ error_description: "Initial access token validation failed"
764
+ });
765
+ }
766
+ if (!authorization) throw registrationBearerError("UNAUTHORIZED", "invalid_token", "Invalid initial access token");
767
+ return authorization;
768
+ }
769
+ //#endregion
1575
770
  //#region src/register.ts
1576
771
  /**
1577
772
  * Resolves the auth method and type for unauthenticated DCR.
@@ -1588,18 +783,42 @@ function resolveUnauthenticatedAuth(body) {
1588
783
  type: body.type === "web" ? void 0 : body.type
1589
784
  };
1590
785
  }
786
+ const DEFAULT_REGISTRATION_GRANT_TYPES = ["authorization_code"];
787
+ function resolveRegistrationGrantTypes(client) {
788
+ const grantTypes = client.grant_types ?? [...DEFAULT_REGISTRATION_GRANT_TYPES];
789
+ if (grantTypes.length > 0) return grantTypes;
790
+ throw new APIError("BAD_REQUEST", {
791
+ error: "invalid_client_metadata",
792
+ error_description: "grant_types must contain at least one grant type"
793
+ });
794
+ }
795
+ function resolveRegistrationResponseTypes(client, grantTypes) {
796
+ if (client.response_types) return client.response_types;
797
+ return grantTypes.includes("authorization_code") ? ["code"] : void 0;
798
+ }
799
+ function applyOAuthClientRegistrationDefaults(client) {
800
+ const grantTypes = resolveRegistrationGrantTypes(client);
801
+ return {
802
+ ...client,
803
+ token_endpoint_auth_method: client.token_endpoint_auth_method ?? "client_secret_basic",
804
+ grant_types: grantTypes,
805
+ response_types: resolveRegistrationResponseTypes(client, grantTypes)
806
+ };
807
+ }
1591
808
  async function registerEndpoint(ctx, opts) {
809
+ const body = ctx.body;
1592
810
  if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
1593
811
  error: "access_denied",
1594
812
  error_description: "Client registration is disabled"
1595
813
  });
1596
- const body = ctx.body;
1597
814
  const session = await getSessionFromCtx(ctx);
1598
- if (!(session || opts.allowUnauthenticatedClientRegistration)) throw new APIError("UNAUTHORIZED", {
1599
- error: "invalid_token",
1600
- error_description: "Authentication required for client registration"
815
+ const tokenAuthorization = session ? void 0 : await authorizeInitialAccessToken(ctx, opts, body);
816
+ const isTokenAuthorized = Boolean(tokenAuthorization);
817
+ if (!(session || isTokenAuthorized || opts.allowUnauthenticatedClientRegistration)) throw new APIError("UNAUTHORIZED", { error_description: "Authentication required for client registration" }, {
818
+ "WWW-Authenticate": "Bearer",
819
+ ...NO_STORE_HEADERS
1601
820
  });
1602
- if (!session) {
821
+ if (!session && !isTokenAuthorized) {
1603
822
  if (body.grant_types?.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
1604
823
  error: "invalid_client_metadata",
1605
824
  error_description: "client_credentials grant requires authenticated registration"
@@ -1609,47 +828,83 @@ async function registerEndpoint(ctx, opts) {
1609
828
  body.type = resolved.type;
1610
829
  }
1611
830
  if (!body.scope) body.scope = (opts.clientRegistrationDefaultScopes ?? opts.scopes)?.join(" ");
1612
- return createOAuthClientEndpoint(ctx, opts, { isRegister: true });
831
+ const requestedResources = Array.isArray(body.resources) ? [...new Set(body.resources.filter((resource) => typeof resource === "string" && resource.length > 0))] : [];
832
+ if (requestedResources.length > 0) for (const identifier of requestedResources) {
833
+ const row = await getResource(ctx, opts, identifier);
834
+ if (!row) throw new APIError("BAD_REQUEST", {
835
+ error: "invalid_target",
836
+ error_description: `requested resource ${identifier} does not exist`
837
+ });
838
+ if (row.disabled) throw new APIError("BAD_REQUEST", {
839
+ error: "invalid_target",
840
+ error_description: `requested resource ${identifier} is disabled`
841
+ });
842
+ }
843
+ return createOAuthClientEndpoint(ctx, opts, {
844
+ isRegister: true,
845
+ session,
846
+ referenceId: tokenAuthorization?.referenceId,
847
+ resources: requestedResources.length > 0 ? requestedResources : void 0
848
+ });
1613
849
  }
1614
850
  async function checkOAuthClient(client, opts, settings) {
1615
- const isPublic = client.token_endpoint_auth_method === "none";
1616
- if (client.type) {
1617
- if (isPublic && !(client.type === "native" || client.type === "user-agent-based")) throw new APIError("BAD_REQUEST", {
851
+ const clientWithDefaults = applyOAuthClientRegistrationDefaults(client);
852
+ const isPublic = clientWithDefaults.token_endpoint_auth_method === "none";
853
+ const tokenEndpointAuthMethod = clientWithDefaults.token_endpoint_auth_method ?? "client_secret_basic";
854
+ if (!new Set(getSupportedAuthMethods(opts, { includeNone: true })).has(tokenEndpointAuthMethod)) throw new APIError("BAD_REQUEST", {
855
+ error: "invalid_client_metadata",
856
+ error_description: `unsupported token_endpoint_auth_method ${tokenEndpointAuthMethod}`
857
+ });
858
+ if (clientWithDefaults.dpop_bound_access_tokens !== void 0 && typeof clientWithDefaults.dpop_bound_access_tokens !== "boolean") throw new APIError("BAD_REQUEST", {
859
+ error: "invalid_client_metadata",
860
+ error_description: "dpop_bound_access_tokens must be a boolean"
861
+ });
862
+ if (clientWithDefaults.type) {
863
+ if (isPublic && !(clientWithDefaults.type === "native" || clientWithDefaults.type === "user-agent-based")) throw new APIError("BAD_REQUEST", {
1618
864
  error: "invalid_client_metadata",
1619
865
  error_description: `Type must be 'native' or 'user-agent-based' for public applications`
1620
866
  });
1621
- else if (!isPublic && !(client.type === "web")) throw new APIError("BAD_REQUEST", {
867
+ else if (!isPublic && !(clientWithDefaults.type === "web")) throw new APIError("BAD_REQUEST", {
1622
868
  error: "invalid_client_metadata",
1623
869
  error_description: `Type must be 'web' for confidential applications`
1624
870
  });
1625
871
  }
1626
- if ((!client.grant_types || client.grant_types.includes("authorization_code")) && (!client.redirect_uris || client.redirect_uris.length === 0)) throw new APIError("BAD_REQUEST", {
872
+ const grantTypes = clientWithDefaults.grant_types ?? [];
873
+ const responseTypes = clientWithDefaults.response_types;
874
+ if (grantTypes.includes("authorization_code") && (!clientWithDefaults.redirect_uris || clientWithDefaults.redirect_uris.length === 0)) throw new APIError("BAD_REQUEST", {
1627
875
  error: "invalid_redirect_uri",
1628
876
  error_description: "Redirect URIs are required for authorization_code and implicit grant types"
1629
877
  });
1630
- const grantTypes = client.grant_types ?? ["authorization_code"];
1631
- const responseTypes = client.response_types ?? ["code"];
1632
- if (grantTypes.includes("authorization_code") && !responseTypes.includes("code")) throw new APIError("BAD_REQUEST", {
878
+ const supportedGrantTypes = new Set(getSupportedGrantTypes(opts));
879
+ for (const grantType of grantTypes) if (!supportedGrantTypes.has(grantType)) throw new APIError("BAD_REQUEST", {
880
+ error: "invalid_client_metadata",
881
+ error_description: `unsupported grant_type ${grantType}`
882
+ });
883
+ if (grantTypes.includes("authorization_code") && !responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
1633
884
  error: "invalid_client_metadata",
1634
885
  error_description: "When 'authorization_code' grant type is used, 'code' response type must be included"
1635
886
  });
1636
- if (client.subject_type !== void 0) {
1637
- if (client.subject_type !== "public" && client.subject_type !== "pairwise") throw new APIError("BAD_REQUEST", {
887
+ if (!grantTypes.includes("authorization_code") && responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
888
+ error: "invalid_client_metadata",
889
+ error_description: "When 'code' response type is used, 'authorization_code' grant type must be included"
890
+ });
891
+ if (clientWithDefaults.subject_type !== void 0) {
892
+ if (clientWithDefaults.subject_type !== "public" && clientWithDefaults.subject_type !== "pairwise") throw new APIError("BAD_REQUEST", {
1638
893
  error: "invalid_client_metadata",
1639
894
  error_description: `subject_type must be "public" or "pairwise"`
1640
895
  });
1641
- if (client.subject_type === "pairwise" && !opts.pairwiseSecret) throw new APIError("BAD_REQUEST", {
896
+ if (clientWithDefaults.subject_type === "pairwise" && !opts.pairwiseSecret) throw new APIError("BAD_REQUEST", {
1642
897
  error: "invalid_client_metadata",
1643
898
  error_description: "pairwise subject_type requires server pairwiseSecret configuration"
1644
899
  });
1645
- if (client.subject_type === "pairwise" && client.redirect_uris && client.redirect_uris.length > 1) {
1646
- if (new Set(client.redirect_uris.map((uri) => new URL(uri).host)).size > 1) throw new APIError("BAD_REQUEST", {
900
+ if (clientWithDefaults.subject_type === "pairwise" && clientWithDefaults.redirect_uris && clientWithDefaults.redirect_uris.length > 1) {
901
+ if (new Set(clientWithDefaults.redirect_uris.map((uri) => new URL(uri).host)).size > 1) throw new APIError("BAD_REQUEST", {
1647
902
  error: "invalid_client_metadata",
1648
903
  error_description: "pairwise clients with redirect_uris on different hosts require a sector_identifier_uri, which is not yet supported. All redirect_uris must share the same host."
1649
904
  });
1650
905
  }
1651
906
  }
1652
- const requestedScopes = (client?.scope)?.split(" ").filter((v) => v.length);
907
+ const requestedScopes = (clientWithDefaults?.scope)?.split(" ").filter((v) => v.length);
1653
908
  const allowedScopes = settings?.isRegister ? opts.clientRegistrationAllowedScopes ?? opts.scopes : opts.scopes;
1654
909
  if (allowedScopes) {
1655
910
  const validScopes = new Set(allowedScopes);
@@ -1658,21 +913,22 @@ async function checkOAuthClient(client, opts, settings) {
1658
913
  error_description: `cannot request scope ${requestedScope}`
1659
914
  });
1660
915
  }
1661
- if (settings?.isRegister && client.require_pkce === false) throw new APIError("BAD_REQUEST", {
916
+ if (settings?.isRegister && clientWithDefaults.require_pkce === false) throw new APIError("BAD_REQUEST", {
1662
917
  error: "invalid_client_metadata",
1663
918
  error_description: `pkce is required for registered clients.`
1664
919
  });
1665
- if (client.token_endpoint_auth_method === "private_key_jwt") {
1666
- if (client.jwks && client.jwks_uri) throw new APIError("BAD_REQUEST", {
920
+ const usesAssertionKeyMaterial = tokenEndpointAuthMethod === "private_key_jwt" || isExtensionTokenEndpointAuthMethod(opts, tokenEndpointAuthMethod);
921
+ if (clientWithDefaults.jwks || clientWithDefaults.jwks_uri) {
922
+ if (!usesAssertionKeyMaterial) throw new APIError("BAD_REQUEST", {
1667
923
  error: "invalid_client_metadata",
1668
- error_description: "jwks and jwks_uri are mutually exclusive"
924
+ error_description: "jwks and jwks_uri are only allowed with private_key_jwt or an assertion-based authentication method"
1669
925
  });
1670
- if (!client.jwks && !client.jwks_uri) throw new APIError("BAD_REQUEST", {
926
+ if (clientWithDefaults.jwks && clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
1671
927
  error: "invalid_client_metadata",
1672
- error_description: "private_key_jwt requires either jwks or jwks_uri"
928
+ error_description: "jwks and jwks_uri are mutually exclusive"
1673
929
  });
1674
- if (client.jwks_uri) try {
1675
- const uri = new URL(client.jwks_uri);
930
+ if (clientWithDefaults.jwks_uri) try {
931
+ const uri = new URL(clientWithDefaults.jwks_uri);
1676
932
  if (uri.protocol !== "https:") throw new APIError("BAD_REQUEST", {
1677
933
  error: "invalid_client_metadata",
1678
934
  error_description: "jwks_uri must use HTTPS"
@@ -1692,25 +948,26 @@ async function checkOAuthClient(client, opts, settings) {
1692
948
  error_description: "jwks_uri must be a valid URL"
1693
949
  });
1694
950
  }
1695
- if (client.jwks) {
1696
- const keys = Array.isArray(client.jwks) ? client.jwks : client.jwks.keys;
951
+ if (clientWithDefaults.jwks) {
952
+ const keys = Array.isArray(clientWithDefaults.jwks) ? clientWithDefaults.jwks : clientWithDefaults.jwks.keys;
1697
953
  if (!Array.isArray(keys) || keys.length === 0) throw new APIError("BAD_REQUEST", {
1698
954
  error: "invalid_client_metadata",
1699
955
  error_description: "jwks must be a non-empty array of JWK objects or a JWKS document {keys:[...]}"
1700
956
  });
1701
957
  }
1702
- } else if (client.jwks || client.jwks_uri) throw new APIError("BAD_REQUEST", {
958
+ }
959
+ if (tokenEndpointAuthMethod === "private_key_jwt" && !clientWithDefaults.jwks && !clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
1703
960
  error: "invalid_client_metadata",
1704
- error_description: "jwks and jwks_uri are only allowed with private_key_jwt authentication"
961
+ error_description: "private_key_jwt requires either jwks or jwks_uri"
1705
962
  });
1706
- if (client.backchannel_logout_uri !== void 0) {
963
+ if (clientWithDefaults.backchannel_logout_uri !== void 0) {
1707
964
  if (opts.disableJwtPlugin) throw new APIError("BAD_REQUEST", {
1708
965
  error: "invalid_client_metadata",
1709
966
  error_description: "backchannel_logout_uri requires the jwt plugin (disableJwtPlugin must be false)"
1710
967
  });
1711
968
  let url;
1712
969
  try {
1713
- url = new URL(client.backchannel_logout_uri);
970
+ url = new URL(clientWithDefaults.backchannel_logout_uri);
1714
971
  } catch {
1715
972
  throw new APIError("BAD_REQUEST", {
1716
973
  error: "invalid_client_metadata",
@@ -1721,7 +978,7 @@ async function checkOAuthClient(client, opts, settings) {
1721
978
  error: "invalid_client_metadata",
1722
979
  error_description: "backchannel_logout_uri must use http or https"
1723
980
  });
1724
- if (client.backchannel_logout_uri.includes("#")) throw new APIError("BAD_REQUEST", {
981
+ if (clientWithDefaults.backchannel_logout_uri.includes("#")) throw new APIError("BAD_REQUEST", {
1725
982
  error: "invalid_client_metadata",
1726
983
  error_description: "backchannel_logout_uri must not include a fragment component"
1727
984
  });
@@ -1737,27 +994,27 @@ async function checkOAuthClient(client, opts, settings) {
1737
994
  }
1738
995
  }
1739
996
  async function createOAuthClientEndpoint(ctx, opts, settings) {
1740
- const body = ctx.body;
1741
- const session = await getSessionFromCtx(ctx);
1742
- if (settings.isRegister) {
1743
- if (session) await assertClientPrivileges(ctx, session, opts, "create");
1744
- } else await assertClientPrivileges(ctx, session, opts, "create");
997
+ const body = applyOAuthClientRegistrationDefaults(ctx.body);
998
+ const session = settings.session !== void 0 ? settings.session : await getSessionFromCtx(ctx);
999
+ if (!settings.isRegister || session) await assertClientPrivileges(ctx, session, opts, "create");
1745
1000
  const isPublic = body.token_endpoint_auth_method === "none";
1746
1001
  const isPrivateKeyJwt = body.token_endpoint_auth_method === "private_key_jwt";
1747
- await checkOAuthClient(ctx.body, opts, {
1002
+ const isExtensionAuthMethod = isExtensionTokenEndpointAuthMethod(opts, body.token_endpoint_auth_method);
1003
+ await checkOAuthClient(body, opts, {
1748
1004
  ...settings,
1749
1005
  ctx
1750
1006
  });
1751
1007
  const clientId = opts.generateClientId?.() || generateRandomString(32, "a-z", "A-Z");
1752
- const clientSecret = isPublic || isPrivateKeyJwt ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
1008
+ const clientSecret = isPublic || isPrivateKeyJwt || isExtensionAuthMethod ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
1753
1009
  const storedClientSecret = clientSecret ? await storeClientSecret(ctx, opts, clientSecret) : void 0;
1754
1010
  const iat = Math.floor(Date.now() / 1e3);
1755
- const referenceId = opts.clientReference ? await opts.clientReference({
1756
- user: session?.user,
1757
- session: session?.session
1758
- }) : void 0;
1011
+ const referenceId = settings.referenceId ?? (session && opts.clientReference ? await opts.clientReference({
1012
+ user: session.user,
1013
+ session: session.session
1014
+ }) : void 0);
1759
1015
  const schema = oauthToSchema({
1760
- ...body ?? {},
1016
+ ...body,
1017
+ redirect_uris: body.redirect_uris ?? [],
1761
1018
  disabled: void 0,
1762
1019
  client_secret_expires_at: storedClientSecret ? settings.isRegister && opts?.clientRegistrationClientSecretExpiration ? toExpJWT(opts.clientRegistrationClientSecretExpiration, iat) : 0 : void 0,
1763
1020
  client_id: clientId,
@@ -1767,24 +1024,38 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1767
1024
  user_id: referenceId ? void 0 : session?.session.userId,
1768
1025
  reference_id: referenceId
1769
1026
  });
1770
- const client = await ctx.context.adapter.create({
1771
- model: "oauthClient",
1772
- data: {
1773
- ...schema,
1774
- createdAt: /* @__PURE__ */ new Date(iat * 1e3),
1775
- updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
1776
- }
1777
- });
1778
- return ctx.json(schemaToOAuth({
1779
- ...client,
1027
+ const resources = settings.resources ?? [];
1028
+ const responseBody = schemaToOAuth({
1029
+ ...await runWithTransaction(ctx.context.adapter, async () => {
1030
+ const createdClient = await ctx.context.adapter.create({
1031
+ model: "oauthClient",
1032
+ data: {
1033
+ ...schema,
1034
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3),
1035
+ updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
1036
+ }
1037
+ });
1038
+ if (resources.length > 0) {
1039
+ const linkModel = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
1040
+ const now = /* @__PURE__ */ new Date();
1041
+ for (const resourceId of resources) await ctx.context.adapter.create({
1042
+ model: linkModel,
1043
+ forceAllowId: true,
1044
+ data: {
1045
+ id: buildClientResourceLinkId(clientId, resourceId),
1046
+ clientId,
1047
+ resourceId,
1048
+ createdAt: now
1049
+ }
1050
+ });
1051
+ }
1052
+ return createdClient;
1053
+ }),
1780
1054
  clientSecret: clientSecret ? (opts.prefix?.clientSecret ?? "") + clientSecret : void 0
1781
- }), {
1782
- status: 201,
1783
- headers: {
1784
- "Cache-Control": "no-store",
1785
- Pragma: "no-cache"
1786
- }
1787
1055
  });
1056
+ if (resources.length > 0) responseBody.resources = resources;
1057
+ ctx.setStatus(201);
1058
+ return ctx.json(responseBody);
1788
1059
  }
1789
1060
  /**
1790
1061
  * Converts an OAuth 2.0 Dynamic Client Schema to a Database Schema
@@ -1793,7 +1064,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1793
1064
  * @returns
1794
1065
  */
1795
1066
  function oauthToSchema(input) {
1796
- const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: inputJwks, jwks_uri: jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, backchannel_logout_uri: backchannelLogoutUri, backchannel_logout_session_required: backchannelLogoutSessionRequired, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1067
+ const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: inputJwks, jwks_uri: jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, backchannel_logout_uri: backchannelLogoutUri, backchannel_logout_session_required: backchannelLogoutSessionRequired, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, dpop_bound_access_tokens: dpopBoundAccessTokens, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1797
1068
  const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
1798
1069
  const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
1799
1070
  const scopes = _scope?.split(" ");
@@ -1833,6 +1104,7 @@ function oauthToSchema(input) {
1833
1104
  skipConsent,
1834
1105
  enableEndSession,
1835
1106
  requirePKCE,
1107
+ dpopBoundAccessTokens,
1836
1108
  subjectType,
1837
1109
  referenceId,
1838
1110
  metadata
@@ -1845,7 +1117,7 @@ function oauthToSchema(input) {
1845
1117
  * @returns
1846
1118
  */
1847
1119
  function schemaToOAuth(input) {
1848
- const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, backchannelLogoutUri, backchannelLogoutSessionRequired, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, subjectType, referenceId, metadata } = input;
1120
+ const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, backchannelLogoutUri, backchannelLogoutSessionRequired, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, dpopBoundAccessTokens, subjectType, referenceId, metadata } = input;
1849
1121
  const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
1850
1122
  const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
1851
1123
  const _scopes = scopes?.join(" ");
@@ -1881,6 +1153,7 @@ function schemaToOAuth(input) {
1881
1153
  skip_consent: skipConsent ?? void 0,
1882
1154
  enable_end_session: enableEndSession ?? void 0,
1883
1155
  require_pkce: requirePKCE ?? void 0,
1156
+ dpop_bound_access_tokens: dpopBoundAccessTokens ?? void 0,
1884
1157
  subject_type: subjectType ?? void 0,
1885
1158
  reference_id: referenceId ?? void 0
1886
1159
  };
@@ -2093,10 +1366,12 @@ async function rotateClientSecretEndpoint(ctx, opts) {
2093
1366
  }
2094
1367
  //#endregion
2095
1368
  //#region src/oauthClient/index.ts
1369
+ const tokenEndpointAuthMethodSchema = z.string().trim().min(1);
1370
+ const grantTypesSchema = z.array(z.string().trim().min(1)).min(1);
2096
1371
  const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/create-client", {
2097
1372
  method: "POST",
2098
1373
  body: z.object({
2099
- redirect_uris: z.array(SafeUrlSchema).min(1),
1374
+ redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2100
1375
  scope: z.string().optional(),
2101
1376
  client_name: z.string().optional(),
2102
1377
  client_uri: z.string().optional(),
@@ -2110,20 +1385,11 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
2110
1385
  post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2111
1386
  backchannel_logout_uri: SafeUrlSchema.optional(),
2112
1387
  backchannel_logout_session_required: z.boolean().optional(),
2113
- token_endpoint_auth_method: z.enum([
2114
- "none",
2115
- "client_secret_basic",
2116
- "client_secret_post",
2117
- "private_key_jwt"
2118
- ]).default("client_secret_basic").optional(),
1388
+ token_endpoint_auth_method: tokenEndpointAuthMethodSchema.optional(),
2119
1389
  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(),
2120
1390
  jwks_uri: z.string().optional(),
2121
- grant_types: z.array(z.enum([
2122
- "authorization_code",
2123
- "client_credentials",
2124
- "refresh_token"
2125
- ])).default(["authorization_code"]).optional(),
2126
- response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
1391
+ grant_types: grantTypesSchema.optional(),
1392
+ response_types: z.array(z.enum(["code"])).optional(),
2127
1393
  type: z.enum([
2128
1394
  "web",
2129
1395
  "native",
@@ -2133,14 +1399,16 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
2133
1399
  skip_consent: z.boolean().optional(),
2134
1400
  enable_end_session: z.boolean().optional(),
2135
1401
  require_pkce: z.boolean().optional(),
1402
+ dpop_bound_access_tokens: z.boolean().optional(),
2136
1403
  subject_type: z.enum(["public", "pairwise"]).optional(),
2137
1404
  metadata: z.record(z.string(), z.unknown()).optional()
2138
1405
  }),
2139
1406
  metadata: {
1407
+ noStore: true,
2140
1408
  SERVER_ONLY: true,
2141
1409
  openapi: {
2142
1410
  description: "Register an OAuth2 application",
2143
- responses: { "200": {
1411
+ responses: { "201": {
2144
1412
  description: "OAuth2 application registered successfully",
2145
1413
  content: { "application/json": { schema: {
2146
1414
  type: "object",
@@ -2216,24 +1484,12 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
2216
1484
  },
2217
1485
  token_endpoint_auth_method: {
2218
1486
  type: "string",
2219
- description: "Requested authentication method for the token endpoint",
2220
- enum: [
2221
- "none",
2222
- "client_secret_basic",
2223
- "client_secret_post"
2224
- ]
1487
+ description: "Requested authentication method for the token endpoint"
2225
1488
  },
2226
1489
  grant_types: {
2227
1490
  type: "array",
2228
- items: {
2229
- type: "string",
2230
- enum: [
2231
- "authorization_code",
2232
- "client_credentials",
2233
- "refresh_token"
2234
- ]
2235
- },
2236
- description: "Requested authentication method for the token endpoint"
1491
+ items: { type: "string" },
1492
+ description: "Grant types the client may use at the token endpoint"
2237
1493
  },
2238
1494
  response_types: {
2239
1495
  type: "array",
@@ -2241,7 +1497,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
2241
1497
  type: "string",
2242
1498
  enum: ["code"]
2243
1499
  },
2244
- description: "Requested authentication method for the token endpoint"
1500
+ description: "Response types the client may use at the authorization endpoint"
2245
1501
  },
2246
1502
  public: {
2247
1503
  type: "boolean",
@@ -2284,7 +1540,7 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
2284
1540
  method: "POST",
2285
1541
  use: [sessionMiddleware],
2286
1542
  body: z.object({
2287
- redirect_uris: z.array(SafeUrlSchema).min(1),
1543
+ redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2288
1544
  scope: z.string().optional(),
2289
1545
  client_name: z.string().optional(),
2290
1546
  client_uri: z.string().optional(),
@@ -2298,159 +1554,142 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
2298
1554
  post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2299
1555
  backchannel_logout_uri: SafeUrlSchema.optional(),
2300
1556
  backchannel_logout_session_required: z.boolean().optional(),
2301
- token_endpoint_auth_method: z.enum([
2302
- "none",
2303
- "client_secret_basic",
2304
- "client_secret_post",
2305
- "private_key_jwt"
2306
- ]).default("client_secret_basic").optional(),
1557
+ token_endpoint_auth_method: tokenEndpointAuthMethodSchema.optional(),
2307
1558
  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(),
2308
1559
  jwks_uri: z.string().optional(),
2309
- grant_types: z.array(z.enum([
2310
- "authorization_code",
2311
- "client_credentials",
2312
- "refresh_token"
2313
- ])).default(["authorization_code"]).optional(),
2314
- response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
1560
+ grant_types: grantTypesSchema.optional(),
1561
+ response_types: z.array(z.enum(["code"])).optional(),
2315
1562
  type: z.enum([
2316
1563
  "web",
2317
1564
  "native",
2318
1565
  "user-agent-based"
2319
- ]).optional()
1566
+ ]).optional(),
1567
+ dpop_bound_access_tokens: z.boolean().optional()
2320
1568
  }),
2321
- metadata: { openapi: {
2322
- description: "Register an OAuth2 application",
2323
- responses: { "200": {
2324
- description: "OAuth2 application registered successfully",
2325
- content: { "application/json": { schema: {
2326
- type: "object",
2327
- properties: {
2328
- client_id: {
2329
- type: "string",
2330
- description: "Unique identifier for the client"
2331
- },
2332
- client_secret: {
2333
- type: "string",
2334
- description: "Secret key for the client"
2335
- },
2336
- client_secret_expires_at: {
2337
- type: "number",
2338
- description: "Time the client secret will expire. If 0, the client secret will never expire."
2339
- },
2340
- scope: {
2341
- type: "string",
2342
- description: "Space-separated scopes allowed by the client"
2343
- },
2344
- user_id: {
2345
- type: "string",
2346
- description: "ID of the user who registered the client, null if registered anonymously"
2347
- },
2348
- client_id_issued_at: {
2349
- type: "number",
2350
- description: "Creation timestamp of this client"
2351
- },
2352
- client_name: {
2353
- type: "string",
2354
- description: "Name of the OAuth2 application"
2355
- },
2356
- client_uri: {
2357
- type: "string",
2358
- description: "URI of the OAuth2 application"
2359
- },
2360
- logo_uri: {
2361
- type: "string",
2362
- description: "Icon URI for the application"
2363
- },
2364
- contacts: {
2365
- type: "array",
2366
- items: { type: "string" },
2367
- description: "List representing ways to contact people responsible for this client, typically email addresses"
2368
- },
2369
- tos_uri: {
2370
- type: "string",
2371
- description: "Client's terms of service uri"
2372
- },
2373
- policy_uri: {
2374
- type: "string",
2375
- description: "Client's policy uri"
2376
- },
2377
- software_id: {
2378
- type: "string",
2379
- description: "Unique identifier assigned by the developer to help in the dynamic registration process"
2380
- },
2381
- software_version: {
2382
- type: "string",
2383
- description: "Version identifier for the software_id"
2384
- },
2385
- software_statement: {
2386
- type: "string",
2387
- description: "JWT containing metadata values about the client software as claims"
2388
- },
2389
- redirect_uris: {
2390
- type: "array",
2391
- items: {
1569
+ metadata: {
1570
+ noStore: true,
1571
+ openapi: {
1572
+ description: "Register an OAuth2 application",
1573
+ responses: { "201": {
1574
+ description: "OAuth2 application registered successfully",
1575
+ content: { "application/json": { schema: {
1576
+ type: "object",
1577
+ properties: {
1578
+ client_id: {
2392
1579
  type: "string",
2393
- format: "uri"
1580
+ description: "Unique identifier for the client"
2394
1581
  },
2395
- description: "List of allowed redirect uris"
2396
- },
2397
- token_endpoint_auth_method: {
2398
- type: "string",
2399
- description: "Response types the client may use",
2400
- enum: [
2401
- "none",
2402
- "client_secret_basic",
2403
- "client_secret_post"
2404
- ]
2405
- },
2406
- grant_types: {
2407
- type: "array",
2408
- items: {
1582
+ client_secret: {
1583
+ type: "string",
1584
+ description: "Secret key for the client"
1585
+ },
1586
+ client_secret_expires_at: {
1587
+ type: "number",
1588
+ description: "Time the client secret will expire. If 0, the client secret will never expire."
1589
+ },
1590
+ scope: {
1591
+ type: "string",
1592
+ description: "Space-separated scopes allowed by the client"
1593
+ },
1594
+ user_id: {
1595
+ type: "string",
1596
+ description: "ID of the user who registered the client, null if registered anonymously"
1597
+ },
1598
+ client_id_issued_at: {
1599
+ type: "number",
1600
+ description: "Creation timestamp of this client"
1601
+ },
1602
+ client_name: {
1603
+ type: "string",
1604
+ description: "Name of the OAuth2 application"
1605
+ },
1606
+ client_uri: {
1607
+ type: "string",
1608
+ description: "URI of the OAuth2 application"
1609
+ },
1610
+ logo_uri: {
1611
+ type: "string",
1612
+ description: "Icon URI for the application"
1613
+ },
1614
+ contacts: {
1615
+ type: "array",
1616
+ items: { type: "string" },
1617
+ description: "List representing ways to contact people responsible for this client, typically email addresses"
1618
+ },
1619
+ tos_uri: {
1620
+ type: "string",
1621
+ description: "Client's terms of service uri"
1622
+ },
1623
+ policy_uri: {
1624
+ type: "string",
1625
+ description: "Client's policy uri"
1626
+ },
1627
+ software_id: {
1628
+ type: "string",
1629
+ description: "Unique identifier assigned by the developer to help in the dynamic registration process"
1630
+ },
1631
+ software_version: {
1632
+ type: "string",
1633
+ description: "Version identifier for the software_id"
1634
+ },
1635
+ software_statement: {
1636
+ type: "string",
1637
+ description: "JWT containing metadata values about the client software as claims"
1638
+ },
1639
+ redirect_uris: {
1640
+ type: "array",
1641
+ items: {
1642
+ type: "string",
1643
+ format: "uri"
1644
+ },
1645
+ description: "List of allowed redirect uris"
1646
+ },
1647
+ token_endpoint_auth_method: {
1648
+ type: "string",
1649
+ description: "Requested authentication method for the token endpoint"
1650
+ },
1651
+ grant_types: {
1652
+ type: "array",
1653
+ items: { type: "string" },
1654
+ description: "Grant types the client may use at the token endpoint"
1655
+ },
1656
+ response_types: {
1657
+ type: "array",
1658
+ items: {
1659
+ type: "string",
1660
+ enum: ["code"]
1661
+ },
1662
+ description: "Response types the client may use at the authorization endpoint"
1663
+ },
1664
+ public: {
1665
+ type: "boolean",
1666
+ description: "Whether the client is public as determined by the type"
1667
+ },
1668
+ type: {
2409
1669
  type: "string",
1670
+ description: "Type of the client",
2410
1671
  enum: [
2411
- "authorization_code",
2412
- "client_credentials",
2413
- "refresh_token"
1672
+ "web",
1673
+ "native",
1674
+ "user-agent-based"
2414
1675
  ]
2415
1676
  },
2416
- description: "Requested authentication method for the token endpoint"
2417
- },
2418
- response_types: {
2419
- type: "array",
2420
- items: {
2421
- type: "string",
2422
- enum: ["code"]
1677
+ disabled: {
1678
+ type: "boolean",
1679
+ description: "Whether the client is disabled"
2423
1680
  },
2424
- description: "Requested authentication method for the token endpoint"
2425
- },
2426
- public: {
2427
- type: "boolean",
2428
- description: "Whether the client is public as determined by the type"
2429
- },
2430
- type: {
2431
- type: "string",
2432
- description: "Type of the client",
2433
- enum: [
2434
- "web",
2435
- "native",
2436
- "user-agent-based"
2437
- ]
2438
- },
2439
- disabled: {
2440
- type: "boolean",
2441
- description: "Whether the client is disabled"
1681
+ metadata: {
1682
+ type: "object",
1683
+ additionalProperties: true,
1684
+ nullable: true,
1685
+ description: "Additional metadata for the application"
1686
+ }
2442
1687
  },
2443
- metadata: {
2444
- type: "object",
2445
- additionalProperties: true,
2446
- nullable: true,
2447
- description: "Additional metadata for the application"
2448
- }
2449
- },
2450
- required: ["client_id"]
2451
- } } }
2452
- } }
2453
- } }
1688
+ required: ["client_id"]
1689
+ } } }
1690
+ } }
1691
+ }
1692
+ }
2454
1693
  }, async (ctx) => {
2455
1694
  return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
2456
1695
  });
@@ -2509,11 +1748,7 @@ const adminUpdateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/updat
2509
1748
  post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2510
1749
  backchannel_logout_uri: SafeUrlSchema.optional(),
2511
1750
  backchannel_logout_session_required: z.boolean().optional(),
2512
- grant_types: z.array(z.enum([
2513
- "authorization_code",
2514
- "client_credentials",
2515
- "refresh_token"
2516
- ])).optional(),
1751
+ grant_types: grantTypesSchema.optional(),
2517
1752
  response_types: z.array(z.enum(["code"])).optional(),
2518
1753
  type: z.enum([
2519
1754
  "web",
@@ -2523,6 +1758,7 @@ const adminUpdateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/updat
2523
1758
  client_secret_expires_at: z.union([z.string(), z.number()]).optional(),
2524
1759
  skip_consent: z.boolean().optional(),
2525
1760
  enable_end_session: z.boolean().optional(),
1761
+ dpop_bound_access_tokens: z.boolean().optional(),
2526
1762
  metadata: z.record(z.string(), z.unknown()).optional()
2527
1763
  })
2528
1764
  }),
@@ -2553,11 +1789,7 @@ const updateOAuthClient = (opts) => createAuthEndpoint("/oauth2/update-client",
2553
1789
  post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2554
1790
  backchannel_logout_uri: SafeUrlSchema.optional(),
2555
1791
  backchannel_logout_session_required: z.boolean().optional(),
2556
- grant_types: z.array(z.enum([
2557
- "authorization_code",
2558
- "client_credentials",
2559
- "refresh_token"
2560
- ])).optional(),
1792
+ grant_types: grantTypesSchema.optional(),
2561
1793
  response_types: z.array(z.enum(["code"])).optional(),
2562
1794
  type: z.enum([
2563
1795
  "web",
@@ -2574,7 +1806,10 @@ const rotateClientSecret = (opts) => createAuthEndpoint("/oauth2/client/rotate-s
2574
1806
  method: "POST",
2575
1807
  use: [sessionMiddleware],
2576
1808
  body: z.object({ client_id: z.string() }),
2577
- metadata: { openapi: { description: "Rotates a confidential client's secret" } }
1809
+ metadata: {
1810
+ noStore: true,
1811
+ openapi: { description: "Rotates a confidential client's secret" }
1812
+ }
2578
1813
  }, async (ctx) => {
2579
1814
  return rotateClientSecretEndpoint(ctx, opts);
2580
1815
  });
@@ -2722,6 +1957,333 @@ const deleteOAuthConsent = (opts) => createAuthEndpoint("/oauth2/delete-consent"
2722
1957
  return deleteConsentEndpoint(ctx, opts);
2723
1958
  });
2724
1959
  //#endregion
1960
+ //#region src/oauthResource/endpoints.ts
1961
+ /**
1962
+ * Gate every admin resource endpoint. Mirrors `assertClientPrivileges`:
1963
+ * a missing session → 401; a defined `resourcePrivileges` callback that
1964
+ * returns falsy → 401 with the original action context preserved.
1965
+ *
1966
+ * When `resourcePrivileges` is undefined, the gate degrades to "any
1967
+ * authenticated session can manage resources" — same forgiving default
1968
+ * as `clientPrivileges`. Operators who care about RBAC must define the
1969
+ * callback.
1970
+ *
1971
+ * @internal
1972
+ */
1973
+ async function assertResourcePrivileges(ctx, session, opts, action, resourceId) {
1974
+ if (!session) throw new APIError("UNAUTHORIZED");
1975
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1976
+ if (!opts.resourcePrivileges) return;
1977
+ if (!await opts.resourcePrivileges({
1978
+ headers: ctx.headers,
1979
+ action,
1980
+ session: session.session,
1981
+ user: session.user,
1982
+ resourceId
1983
+ })) throw new APIError("UNAUTHORIZED");
1984
+ }
1985
+ const resourceModel = (opts) => opts.schema?.oauthResource?.modelName ?? "oauthResource";
1986
+ const linkModel = (opts) => opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
1987
+ const clientModel = (opts) => opts.schema?.oauthClient?.modelName ?? "oauthClient";
1988
+ /**
1989
+ * Decode a URL path-segment parameter.
1990
+ *
1991
+ * better-call's router (better-call@1.3.5) does NOT decode path params —
1992
+ * `tryDecode` is wired into cookie parsing only. So a raw HTTP caller hitting
1993
+ * `/admin/oauth2/resources/https%3A%2F%2Fapi.example.com` lands here with
1994
+ * `ctx.params.identifier === "https%3A%2F%2Fapi.example.com"`, which never
1995
+ * matches the stored `https://api.example.com` row. Decode every path
1996
+ * segment that holds a URI-valued identifier so the admin handlers behave
1997
+ * identically to in-process `auth.api.*` calls (which pass already-decoded
1998
+ * JS strings).
1999
+ *
2000
+ * Falls back to the raw string when decode fails so a malformed identifier
2001
+ * surfaces as a clean NOT_FOUND from the row lookup rather than a 500.
2002
+ *
2003
+ * @internal
2004
+ */
2005
+ function decodePathParam(value) {
2006
+ try {
2007
+ return decodeURIComponent(value);
2008
+ } catch {
2009
+ return value;
2010
+ }
2011
+ }
2012
+ /**
2013
+ * Builds the create-payload from a normalized input. Fills in defaults
2014
+ * for the fields seedResources uses so the admin-CRUD and seed paths
2015
+ * write identical rows.
2016
+ *
2017
+ * @internal
2018
+ */
2019
+ function buildResourceRow(input, now) {
2020
+ return {
2021
+ identifier: input.identifier,
2022
+ name: input.name ?? input.identifier,
2023
+ accessTokenTtl: input.accessTokenTtl ?? null,
2024
+ refreshTokenTtl: input.refreshTokenTtl ?? null,
2025
+ signingAlgorithm: input.signingAlgorithm ?? null,
2026
+ signingKeyId: input.signingKeyId ?? null,
2027
+ allowedScopes: input.allowedScopes ?? null,
2028
+ customClaims: input.customClaims ?? null,
2029
+ dpopBoundAccessTokensRequired: input.dpopBoundAccessTokensRequired ?? false,
2030
+ disabled: input.disabled ?? false,
2031
+ policyVersion: 1,
2032
+ metadata: input.metadata ?? null,
2033
+ createdAt: now,
2034
+ updatedAt: now
2035
+ };
2036
+ }
2037
+ async function createResourceEndpoint(ctx, opts) {
2038
+ await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
2039
+ const input = ctx.body;
2040
+ if (!input?.identifier) throw new APIError("BAD_REQUEST", {
2041
+ error: "invalid_request",
2042
+ error_description: "identifier is required"
2043
+ });
2044
+ await assertIdentifierValid(opts, input.identifier);
2045
+ const now = /* @__PURE__ */ new Date();
2046
+ let created;
2047
+ try {
2048
+ created = await ctx.context.adapter.create({
2049
+ model: resourceModel(opts),
2050
+ data: buildResourceRow(input, now)
2051
+ });
2052
+ } catch (err) {
2053
+ const message = err instanceof Error ? err.message : String(err);
2054
+ if (/unique|duplicate|UNIQUE/i.test(message)) throw new APIError("BAD_REQUEST", {
2055
+ error: "invalid_request",
2056
+ error_description: `resource ${input.identifier} already exists`
2057
+ });
2058
+ throw err;
2059
+ }
2060
+ if (!created) throw new APIError("BAD_REQUEST", {
2061
+ error: "invalid_request",
2062
+ error_description: `resource ${input.identifier} could not be created`
2063
+ });
2064
+ invalidateResourceCache(created.identifier);
2065
+ return ctx.json(created, { status: 201 });
2066
+ }
2067
+ async function listResourcesEndpoint(ctx, opts) {
2068
+ await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "list");
2069
+ const rows = await ctx.context.adapter.findMany({ model: resourceModel(opts) });
2070
+ return ctx.json(rows ?? []);
2071
+ }
2072
+ async function getResourceByIdentifierEndpoint(ctx, opts) {
2073
+ const identifier = decodePathParam(ctx.params.identifier);
2074
+ await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "read", identifier);
2075
+ const row = await ctx.context.adapter.findOne({
2076
+ model: resourceModel(opts),
2077
+ where: [{
2078
+ field: "identifier",
2079
+ value: identifier
2080
+ }]
2081
+ });
2082
+ if (!row) throw new APIError("NOT_FOUND", {
2083
+ error: "not_found",
2084
+ error_description: `resource ${identifier} not found`
2085
+ });
2086
+ return ctx.json(row);
2087
+ }
2088
+ async function updateResourceEndpoint(ctx, opts) {
2089
+ const identifier = decodePathParam(ctx.params.identifier);
2090
+ await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "update", identifier);
2091
+ if (!await ctx.context.adapter.findOne({
2092
+ model: resourceModel(opts),
2093
+ where: [{
2094
+ field: "identifier",
2095
+ value: identifier
2096
+ }]
2097
+ })) throw new APIError("NOT_FOUND", {
2098
+ error: "not_found",
2099
+ error_description: `resource ${identifier} not found`
2100
+ });
2101
+ const update = { updatedAt: /* @__PURE__ */ new Date() };
2102
+ for (const key of [
2103
+ "name",
2104
+ "accessTokenTtl",
2105
+ "refreshTokenTtl",
2106
+ "signingAlgorithm",
2107
+ "signingKeyId",
2108
+ "allowedScopes",
2109
+ "customClaims",
2110
+ "dpopBoundAccessTokensRequired",
2111
+ "disabled",
2112
+ "metadata"
2113
+ ]) if (Object.prototype.hasOwnProperty.call(ctx.body, key)) update[key] = ctx.body[key];
2114
+ await ctx.context.adapter.update({
2115
+ model: resourceModel(opts),
2116
+ where: [{
2117
+ field: "identifier",
2118
+ value: identifier
2119
+ }],
2120
+ update
2121
+ });
2122
+ invalidateResourceCache(identifier);
2123
+ const refreshed = await ctx.context.adapter.findOne({
2124
+ model: resourceModel(opts),
2125
+ where: [{
2126
+ field: "identifier",
2127
+ value: identifier
2128
+ }]
2129
+ });
2130
+ if (!refreshed) throw new APIError("NOT_FOUND", {
2131
+ error: "not_found",
2132
+ error_description: `resource ${identifier} not found`
2133
+ });
2134
+ return ctx.json(refreshed);
2135
+ }
2136
+ async function deleteResourceEndpoint(ctx, opts) {
2137
+ const identifier = decodePathParam(ctx.params.identifier);
2138
+ await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "delete", identifier);
2139
+ if (!await ctx.context.adapter.findOne({
2140
+ model: resourceModel(opts),
2141
+ where: [{
2142
+ field: "identifier",
2143
+ value: identifier
2144
+ }]
2145
+ })) throw new APIError("NOT_FOUND", {
2146
+ error: "not_found",
2147
+ error_description: `resource ${identifier} not found`
2148
+ });
2149
+ await ctx.context.adapter.delete({
2150
+ model: resourceModel(opts),
2151
+ where: [{
2152
+ field: "identifier",
2153
+ value: identifier
2154
+ }]
2155
+ });
2156
+ invalidateResourceCache(identifier);
2157
+ return ctx.json({ deleted: true });
2158
+ }
2159
+ /**
2160
+ * Link a client to a resource.
2161
+ *
2162
+ * Route: `POST /admin/oauth2/resources/:identifier/clients/:client_id`.
2163
+ * Path params carry the identifiers (RESTful linkage) — no body required.
2164
+ * Used by admins when {@link OAuthOptions.enforcePerClientResources} is on.
2165
+ */
2166
+ async function linkClientResourceEndpoint(ctx, opts) {
2167
+ const session = await getSessionFromCtx(ctx);
2168
+ const resourceId = decodePathParam(ctx.params.identifier);
2169
+ const clientId = decodePathParam(ctx.params.client_id);
2170
+ await assertResourcePrivileges(ctx, session, opts, "link", resourceId);
2171
+ if (!await ctx.context.adapter.findOne({
2172
+ model: resourceModel(opts),
2173
+ where: [{
2174
+ field: "identifier",
2175
+ value: resourceId
2176
+ }]
2177
+ })) throw new APIError("NOT_FOUND", {
2178
+ error: "not_found",
2179
+ error_description: `resource ${resourceId} not found`
2180
+ });
2181
+ if (!await ctx.context.adapter.findOne({
2182
+ model: clientModel(opts),
2183
+ where: [{
2184
+ field: "clientId",
2185
+ value: clientId
2186
+ }]
2187
+ })) throw new APIError("NOT_FOUND", {
2188
+ error: "not_found",
2189
+ error_description: `client ${clientId} not found`
2190
+ });
2191
+ const id = buildClientResourceLinkId(clientId, resourceId);
2192
+ try {
2193
+ await ctx.context.adapter.create({
2194
+ model: linkModel(opts),
2195
+ forceAllowId: true,
2196
+ data: {
2197
+ id,
2198
+ clientId,
2199
+ resourceId,
2200
+ createdAt: /* @__PURE__ */ new Date()
2201
+ }
2202
+ });
2203
+ } catch (err) {
2204
+ const message = err instanceof Error ? err.message : String(err);
2205
+ if (/unique|duplicate|UNIQUE/i.test(message)) return ctx.json({
2206
+ linked: true,
2207
+ alreadyLinked: true
2208
+ });
2209
+ throw err;
2210
+ }
2211
+ return ctx.json({ linked: true });
2212
+ }
2213
+ /**
2214
+ * Unlink a client from a resource.
2215
+ *
2216
+ * Route: `DELETE /admin/oauth2/resources/:identifier/clients/:client_id`.
2217
+ * Path params carry the identifiers (RESTful linkage) — no body required.
2218
+ */
2219
+ async function unlinkClientResourceEndpoint(ctx, opts) {
2220
+ const session = await getSessionFromCtx(ctx);
2221
+ const resourceId = decodePathParam(ctx.params.identifier);
2222
+ const clientId = decodePathParam(ctx.params.client_id);
2223
+ await assertResourcePrivileges(ctx, session, opts, "unlink", resourceId);
2224
+ await ctx.context.adapter.deleteMany({
2225
+ model: linkModel(opts),
2226
+ where: [{
2227
+ field: "clientId",
2228
+ value: clientId
2229
+ }, {
2230
+ field: "resourceId",
2231
+ value: resourceId
2232
+ }]
2233
+ });
2234
+ return ctx.json({ unlinked: true });
2235
+ }
2236
+ //#endregion
2237
+ //#region src/oauthResource/index.ts
2238
+ /**
2239
+ * Shared body schema for create/update — every field is optional except
2240
+ * `identifier` on create (validated in the handler). Update accepts a
2241
+ * subset; the handler only writes fields that are explicitly present.
2242
+ */
2243
+ const resourceBodySchema = z.object({
2244
+ identifier: z.string().min(1).optional(),
2245
+ name: z.string().optional(),
2246
+ accessTokenTtl: z.number().int().positive().nullable().optional(),
2247
+ refreshTokenTtl: z.number().int().positive().nullable().optional(),
2248
+ signingAlgorithm: z.enum(JWS_ALGORITHMS).nullable().optional(),
2249
+ signingKeyId: z.string().nullable().optional(),
2250
+ allowedScopes: z.array(z.string()).nullable().optional(),
2251
+ customClaims: z.record(z.string(), z.unknown()).nullable().optional(),
2252
+ dpopBoundAccessTokensRequired: z.boolean().optional(),
2253
+ disabled: z.boolean().optional(),
2254
+ metadata: z.record(z.string(), z.unknown()).nullable().optional()
2255
+ });
2256
+ const adminCreateOAuthResource = (opts) => createAuthEndpoint("/admin/oauth2/resources", {
2257
+ method: "POST",
2258
+ body: resourceBodySchema.required({ identifier: true }),
2259
+ metadata: { SERVER_ONLY: true }
2260
+ }, async (ctx) => createResourceEndpoint(ctx, opts));
2261
+ const adminListOAuthResources = (opts) => createAuthEndpoint("/admin/oauth2/resources", {
2262
+ method: "GET",
2263
+ metadata: { SERVER_ONLY: true }
2264
+ }, async (ctx) => listResourcesEndpoint(ctx, opts));
2265
+ const adminGetOAuthResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier", {
2266
+ method: "GET",
2267
+ metadata: { SERVER_ONLY: true }
2268
+ }, async (ctx) => getResourceByIdentifierEndpoint(ctx, opts));
2269
+ const adminUpdateOAuthResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier", {
2270
+ method: "PATCH",
2271
+ body: resourceBodySchema,
2272
+ metadata: { SERVER_ONLY: true }
2273
+ }, async (ctx) => updateResourceEndpoint(ctx, opts));
2274
+ const adminDeleteOAuthResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier", {
2275
+ method: "DELETE",
2276
+ metadata: { SERVER_ONLY: true }
2277
+ }, async (ctx) => deleteResourceEndpoint(ctx, opts));
2278
+ const adminLinkClientResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier/clients/:client_id", {
2279
+ method: "POST",
2280
+ metadata: { SERVER_ONLY: true }
2281
+ }, async (ctx) => linkClientResourceEndpoint(ctx, opts));
2282
+ const adminUnlinkClientResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier/clients/:client_id", {
2283
+ method: "DELETE",
2284
+ metadata: { SERVER_ONLY: true }
2285
+ }, async (ctx) => unlinkClientResourceEndpoint(ctx, opts));
2286
+ //#endregion
2725
2287
  //#region src/revoke.ts
2726
2288
  /**
2727
2289
  * IMPORTANT NOTES:
@@ -2737,23 +2299,23 @@ const deleteOAuthConsent = (opts) => createAuthEndpoint("/oauth2/delete-consent"
2737
2299
  * delete. Once the token is confirmed to be a valid JWT for this server, the
2738
2300
  * endpoint reports `unsupported_token_type` (RFC 7009 §2.2.1) instead of a
2739
2301
  * silent success, so callers can tell that no server-side revocation happened.
2740
- * An expired or wrong-audience JWT is already inactive and still resolves as a
2741
- * successful no-op. Session-bound tokens (carrying `sid`) are cut off early by
2742
- * the session-liveness check in introspection and userinfo.
2302
+ * An expired JWT or a JWT with an audience rejected by the OAuth resource model
2303
+ * is already inactive and still resolves as a successful no-op. Session-bound
2304
+ * tokens (carrying `sid`) are cut off early by the session-liveness check in
2305
+ * introspection and userinfo.
2743
2306
  */
2744
2307
  async function revokeJwtAccessToken(ctx, opts, token) {
2745
2308
  const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
2746
2309
  const jwtPluginOptions = jwtPlugin?.options;
2747
2310
  try {
2748
- await verifyJwsAccessToken(token, {
2311
+ const verified = await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
2749
2312
  jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
2750
2313
  return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
2751
2314
  },
2752
- verifyOptions: {
2753
- audience: opts.validAudiences ?? ctx.context.baseURL,
2754
- issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL
2755
- }
2756
- });
2315
+ jwksCacheKey: jwtPlugin
2316
+ })), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL });
2317
+ const userInfoAudience = `${ctx.context.baseURL}/oauth2/userinfo`;
2318
+ if (!verified.payload.azp || !await isAudienceClaimAllowed(ctx, opts, verified.payload.aud, [userInfoAudience])) return null;
2757
2319
  } catch (error) {
2758
2320
  if (error instanceof Error) {
2759
2321
  if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
@@ -2831,7 +2393,7 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
2831
2393
  }
2832
2394
  if (!refreshToken.clientId || refreshToken.clientId !== clientId) return null;
2833
2395
  const iat = Math.floor(Date.now() / 1e3);
2834
- if (!await ctx.context.adapter.update({
2396
+ if (!await ctx.context.adapter.incrementOne({
2835
2397
  model: "oauthRefreshToken",
2836
2398
  where: [{
2837
2399
  field: "id",
@@ -2841,7 +2403,8 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
2841
2403
  operator: "eq",
2842
2404
  value: null
2843
2405
  }],
2844
- update: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
2406
+ increment: {},
2407
+ set: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
2845
2408
  })) {
2846
2409
  await invalidateRefreshFamily(ctx, clientId, refreshToken.userId);
2847
2410
  throw new APIError$1("BAD_REQUEST", {
@@ -2884,17 +2447,17 @@ async function revokeAccessToken(ctx, opts, clientId, token) {
2884
2447
  async function revokeEndpoint(ctx, opts) {
2885
2448
  let { token, token_type_hint } = ctx.body;
2886
2449
  if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
2887
- const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/revoke`));
2450
+ const { clientId: client_id, clientSecret: client_secret, preVerified, authMethod } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/revoke`));
2888
2451
  if (!client_id) throw new APIError$1("UNAUTHORIZED", {
2889
2452
  error_description: "missing required credentials",
2890
2453
  error: "invalid_client"
2891
2454
  });
2892
- if (typeof token === "string" && token.startsWith("Bearer ")) token = token.replace("Bearer ", "");
2455
+ if (typeof token === "string") token = stripAccessTokenAuthorizationScheme(token);
2893
2456
  if (!token?.length) throw new APIError$1("BAD_REQUEST", {
2894
2457
  error_description: "missing a required token for introspection",
2895
2458
  error: "invalid_request"
2896
2459
  });
2897
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
2460
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, void 0, authMethod);
2898
2461
  try {
2899
2462
  if (token_type_hint === void 0 || token_type_hint === "access_token") try {
2900
2463
  return await revokeAccessToken(ctx, opts, client.clientId, token);
@@ -3067,6 +2630,11 @@ const schema = {
3067
2630
  type: "boolean",
3068
2631
  required: false
3069
2632
  },
2633
+ dpopBoundAccessTokens: {
2634
+ type: "boolean",
2635
+ required: false,
2636
+ defaultValue: false
2637
+ },
3070
2638
  referenceId: {
3071
2639
  type: "string",
3072
2640
  required: false
@@ -3077,6 +2645,104 @@ const schema = {
3077
2645
  }
3078
2646
  }
3079
2647
  },
2648
+ oauthResource: {
2649
+ modelName: "oauthResource",
2650
+ fields: {
2651
+ identifier: {
2652
+ type: "string",
2653
+ required: true,
2654
+ unique: true
2655
+ },
2656
+ name: {
2657
+ type: "string",
2658
+ required: true
2659
+ },
2660
+ accessTokenTtl: {
2661
+ type: "number",
2662
+ required: false
2663
+ },
2664
+ refreshTokenTtl: {
2665
+ type: "number",
2666
+ required: false
2667
+ },
2668
+ signingAlgorithm: {
2669
+ type: "string",
2670
+ required: false
2671
+ },
2672
+ signingKeyId: {
2673
+ type: "string",
2674
+ required: false
2675
+ },
2676
+ allowedScopes: {
2677
+ type: "string[]",
2678
+ required: false
2679
+ },
2680
+ customClaims: {
2681
+ type: "json",
2682
+ required: false
2683
+ },
2684
+ dpopBoundAccessTokensRequired: {
2685
+ type: "boolean",
2686
+ required: false,
2687
+ defaultValue: false
2688
+ },
2689
+ disabled: {
2690
+ type: "boolean",
2691
+ required: false,
2692
+ defaultValue: false
2693
+ },
2694
+ createdAt: {
2695
+ type: "date",
2696
+ required: false
2697
+ },
2698
+ updatedAt: {
2699
+ type: "date",
2700
+ required: false
2701
+ },
2702
+ policyVersion: {
2703
+ type: "number",
2704
+ required: false,
2705
+ defaultValue: 1
2706
+ },
2707
+ metadata: {
2708
+ type: "json",
2709
+ required: false
2710
+ }
2711
+ }
2712
+ },
2713
+ oauthClientResource: {
2714
+ modelName: "oauthClientResource",
2715
+ fields: {
2716
+ clientId: {
2717
+ type: "string",
2718
+ required: true,
2719
+ references: {
2720
+ model: "oauthClient",
2721
+ field: "clientId",
2722
+ onDelete: "cascade"
2723
+ },
2724
+ index: true
2725
+ },
2726
+ resourceId: {
2727
+ type: "string",
2728
+ required: true,
2729
+ references: {
2730
+ model: "oauthResource",
2731
+ field: "identifier",
2732
+ onDelete: "cascade"
2733
+ },
2734
+ index: true
2735
+ },
2736
+ metadata: {
2737
+ type: "json",
2738
+ required: false
2739
+ },
2740
+ createdAt: {
2741
+ type: "date",
2742
+ required: false
2743
+ }
2744
+ }
2745
+ },
3080
2746
  oauthRefreshToken: { fields: {
3081
2747
  token: {
3082
2748
  type: "string",
@@ -3129,6 +2795,10 @@ const schema = {
3129
2795
  type: "date",
3130
2796
  required: false
3131
2797
  },
2798
+ confirmation: {
2799
+ type: "json",
2800
+ required: false
2801
+ },
3132
2802
  scopes: {
3133
2803
  type: "string[]",
3134
2804
  required: true
@@ -3192,6 +2862,10 @@ const schema = {
3192
2862
  type: "date",
3193
2863
  required: false
3194
2864
  },
2865
+ confirmation: {
2866
+ type: "json",
2867
+ required: false
2868
+ },
3195
2869
  scopes: {
3196
2870
  type: "string[]",
3197
2871
  required: true
@@ -3245,6 +2919,17 @@ const schema = {
3245
2919
  };
3246
2920
  //#endregion
3247
2921
  //#region src/oauth.ts
2922
+ /**
2923
+ * Default scopes advertised and accepted when a configuration sets none. Shared
2924
+ * with the MCP preset so the resource metadata it serves matches what the
2925
+ * authorization-server metadata advertises.
2926
+ */
2927
+ const DEFAULT_OAUTH_SCOPES = [
2928
+ "openid",
2929
+ "profile",
2930
+ "email",
2931
+ "offline_access"
2932
+ ];
3248
2933
  const oAuthState = defineRequestState(() => null);
3249
2934
  const getOAuthProviderState = oAuthState.get;
3250
2935
  const signedQueryIssuedAtMsKey = "signedQueryIssuedAtMs";
@@ -3266,12 +2951,7 @@ const oauthProvider = (options) => {
3266
2951
  const _allowedScopes = clientRegistrationAllowedScopes ? new Set([...clientRegistrationAllowedScopes, ...options.clientRegistrationDefaultScopes]) : new Set([...options.clientRegistrationDefaultScopes]);
3267
2952
  clientRegistrationAllowedScopes = Array.from(_allowedScopes);
3268
2953
  }
3269
- const scopes = new Set((options.scopes ?? [
3270
- "openid",
3271
- "profile",
3272
- "email",
3273
- "offline_access"
3274
- ]).filter((val) => val.length));
2954
+ const scopes = new Set((options.scopes ?? DEFAULT_OAUTH_SCOPES).filter((val) => val.length));
3275
2955
  if (clientRegistrationAllowedScopes) {
3276
2956
  for (const sc of clientRegistrationAllowedScopes) if (!scopes.has(sc)) throw new BetterAuthError(`clientRegistrationAllowedScope ${sc} not found in scopes`);
3277
2957
  }
@@ -3313,6 +2993,7 @@ const oauthProvider = (options) => {
3313
2993
  claims: Array.from(claims),
3314
2994
  clientRegistrationAllowedScopes
3315
2995
  };
2996
+ validateOAuthProviderExtensions(opts.extensions);
3316
2997
  if (opts.pairwiseSecret && opts.pairwiseSecret.length < 32) throw new BetterAuthError("pairwiseSecret must be at least 32 characters long for adequate HMAC-SHA256 security");
3317
2998
  if (opts.grantTypes && opts.grantTypes.includes("refresh_token") && !opts.grantTypes.includes("authorization_code")) throw new BetterAuthError("refresh_token grant requires authorization_code grant");
3318
2999
  if (opts.disableJwtPlugin && (opts.storeClientSecret === "hashed" || typeof opts.storeClientSecret === "object" && "hash" in opts.storeClientSecret)) throw new BetterAuthError("unable to store hashed secrets because id tokens will be signed with secret");
@@ -3348,16 +3029,7 @@ const oauthProvider = (options) => {
3348
3029
  }
3349
3030
  if (isAuthServerMetadataRequest) {
3350
3031
  if (opts.scopes?.includes("openid")) return { response: createMetadataResponse(oidcServerMetadata(endpointCtx, opts)) };
3351
- return { response: createMetadataResponse({
3352
- ...authServerMetadata(endpointCtx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx)?.options, {
3353
- scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
3354
- dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
3355
- public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
3356
- grant_types_supported: opts.grantTypes,
3357
- jwt_disabled: opts.disableJwtPlugin
3358
- }),
3359
- ...mergeDiscoveryMetadata(opts.clientDiscovery)
3360
- }) };
3032
+ return { response: createMetadataResponse(oauthAuthorizationServerMetadata(endpointCtx, opts)) };
3361
3033
  }
3362
3034
  if (isOpenIdConfigRequest) return { response: createMetadataResponse(oidcServerMetadata(endpointCtx, opts)) };
3363
3035
  };
@@ -3456,7 +3128,7 @@ const oauthProvider = (options) => {
3456
3128
  type: "array",
3457
3129
  items: { type: "string" }
3458
3130
  },
3459
- description: "Requested token resource(s) (ie audience) to obtain a JWT formatted access token. May be supplied multiple times as repeated 'resource' query parameters (RFC 8707) or as an array of strings."
3131
+ description: "Requested protected resource(s) for the access token. May be supplied multiple times as repeated 'resource' query parameters (RFC 8707) or as an array of strings."
3460
3132
  },
3461
3133
  {
3462
3134
  name: "prompt",
@@ -3506,8 +3178,10 @@ const oauthProvider = (options) => {
3506
3178
  version: PACKAGE_VERSION,
3507
3179
  options: opts,
3508
3180
  onRequest: handleIssuerMetadataRequest,
3509
- init: (ctx) => {
3181
+ init: async (ctx) => {
3510
3182
  if (ctx.options.secondaryStorage && ctx.options.session?.storeSessionInDatabase !== true) throw new BetterAuthError("OAuth Provider requires `session.storeSessionInDatabase: true` when using secondaryStorage");
3183
+ await seedResources(ctx, opts);
3184
+ logEnforcePerClientResourcesResolution(opts);
3511
3185
  if (!opts.disableJwtPlugin) {
3512
3186
  const jwtPluginOptions = getJwtPlugin(ctx)?.options;
3513
3187
  const issuer = jwtPluginOptions?.jwt?.issuer ?? ctx.baseURL;
@@ -3589,16 +3263,7 @@ const oauthProvider = (options) => {
3589
3263
  metadata: { SERVER_ONLY: true }
3590
3264
  }, async (ctx) => {
3591
3265
  if (opts.scopes && opts.scopes.includes("openid")) return oidcServerMetadata(ctx, opts);
3592
- else return {
3593
- ...authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
3594
- scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
3595
- dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
3596
- public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
3597
- grant_types_supported: opts.grantTypes,
3598
- jwt_disabled: opts.disableJwtPlugin
3599
- }),
3600
- ...mergeDiscoveryMetadata(opts.clientDiscovery)
3601
- };
3266
+ else return oauthAuthorizationServerMetadata(ctx, opts);
3602
3267
  }),
3603
3268
  getOpenIdConfig: createAuthEndpoint("/.well-known/openid-configuration", {
3604
3269
  method: "GET",
@@ -3663,12 +3328,9 @@ const oauthProvider = (options) => {
3663
3328
  }),
3664
3329
  oauth2Token: createOAuthEndpoint("/oauth2/token", {
3665
3330
  method: "POST",
3331
+ cloneRequest: true,
3666
3332
  body: z.object({
3667
- grant_type: z.string().pipe(z.enum([
3668
- "authorization_code",
3669
- "client_credentials",
3670
- "refresh_token"
3671
- ])),
3333
+ grant_type: z.string().trim().min(1),
3672
3334
  client_id: z.string().optional(),
3673
3335
  client_secret: z.string().optional(),
3674
3336
  client_assertion: z.string().optional(),
@@ -3679,7 +3341,7 @@ const oauthProvider = (options) => {
3679
3341
  refresh_token: z.string().optional(),
3680
3342
  resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional(),
3681
3343
  scope: z.string().optional()
3682
- }),
3344
+ }).passthrough(),
3683
3345
  errorCodesByField: {
3684
3346
  grant_type: {
3685
3347
  missing: "invalid_request",
@@ -3688,9 +3350,17 @@ const oauthProvider = (options) => {
3688
3350
  resource: { invalid: "invalid_target" }
3689
3351
  },
3690
3352
  metadata: {
3353
+ noStore: true,
3691
3354
  allowedMediaTypes: ["application/x-www-form-urlencoded"],
3692
3355
  openapi: {
3693
3356
  description: "Obtain an OAuth2.1 access token",
3357
+ parameters: [{
3358
+ name: "DPoP",
3359
+ in: "header",
3360
+ required: false,
3361
+ schema: { type: "string" },
3362
+ description: "RFC 9449 DPoP proof JWT for issuing DPoP-bound tokens"
3363
+ }],
3694
3364
  requestBody: {
3695
3365
  required: true,
3696
3366
  content: { "application/json": { schema: {
@@ -3698,11 +3368,6 @@ const oauthProvider = (options) => {
3698
3368
  properties: {
3699
3369
  grant_type: {
3700
3370
  type: "string",
3701
- enum: [
3702
- "authorization_code",
3703
- "client_credentials",
3704
- "refresh_token"
3705
- ],
3706
3371
  description: "OAuth2 grant type"
3707
3372
  },
3708
3373
  client_id: {
@@ -3739,7 +3404,7 @@ const oauthProvider = (options) => {
3739
3404
  items: { type: "string" },
3740
3405
  description: "Multiple resources (URLs)"
3741
3406
  }],
3742
- description: "Requested token resource(s) (ie audience) to obtain a JWT formatted access token"
3407
+ description: "Requested protected resource(s) for the access token"
3743
3408
  },
3744
3409
  scope: {
3745
3410
  type: "string",
@@ -3762,7 +3427,7 @@ const oauthProvider = (options) => {
3762
3427
  token_type: {
3763
3428
  type: "string",
3764
3429
  description: "The type of the token issued",
3765
- enum: ["Bearer"]
3430
+ enum: ["Bearer", "DPoP"]
3766
3431
  },
3767
3432
  expires_in: {
3768
3433
  type: "number",
@@ -3804,6 +3469,10 @@ const oauthProvider = (options) => {
3804
3469
  }
3805
3470
  }
3806
3471
  }, async (ctx) => {
3472
+ if (ctx.request) {
3473
+ const repeated = await extractRepeatedResourceFromForm(ctx.request);
3474
+ if (repeated && repeated.length > 1) ctx.body.resource = repeated;
3475
+ }
3807
3476
  return tokenEndpoint(ctx, opts);
3808
3477
  }),
3809
3478
  oauth2Introspect: createOAuthEndpoint("/oauth2/introspect", {
@@ -3817,6 +3486,7 @@ const oauthProvider = (options) => {
3817
3486
  token_type_hint: z.string().optional()
3818
3487
  }),
3819
3488
  metadata: {
3489
+ noStore: true,
3820
3490
  allowedMediaTypes: ["application/x-www-form-urlencoded"],
3821
3491
  openapi: {
3822
3492
  description: "Introspect an OAuth2 access or refresh token",
@@ -3988,90 +3658,99 @@ const oauthProvider = (options) => {
3988
3658
  }),
3989
3659
  oauth2UserInfo: createAuthEndpoint("/oauth2/userinfo", {
3990
3660
  method: ["GET", "POST"],
3991
- metadata: { openapi: {
3992
- description: "Get OpenID Connect user information (UserInfo endpoint)",
3993
- security: [{ bearerAuth: [] }, { OAuth2: [
3994
- "openid",
3995
- "profile",
3996
- "email"
3997
- ] }],
3998
- parameters: [{
3999
- name: "Authorization",
4000
- in: "header",
4001
- required: false,
4002
- schema: { type: "string" },
4003
- description: "Bearer access token"
4004
- }],
4005
- responses: {
4006
- "200": {
4007
- description: "User information retrieved successfully",
4008
- content: { "application/json": { schema: {
4009
- type: "object",
4010
- properties: {
4011
- sub: {
4012
- type: "string",
4013
- description: "Subject identifier (user ID)"
4014
- },
4015
- email: {
4016
- type: "string",
4017
- format: "email",
4018
- nullable: true,
4019
- description: "User's email address, included if 'email' scope is granted"
4020
- },
4021
- name: {
4022
- type: "string",
4023
- nullable: true,
4024
- description: "User's full name, included if 'profile' scope is granted"
4025
- },
4026
- picture: {
4027
- type: "string",
4028
- format: "uri",
4029
- nullable: true,
4030
- description: "User's profile picture URL, included if 'profile' scope is granted"
3661
+ metadata: {
3662
+ noStore: true,
3663
+ openapi: {
3664
+ description: "Get OpenID Connect user information (UserInfo endpoint)",
3665
+ security: [{ bearerAuth: [] }, { OAuth2: [
3666
+ "openid",
3667
+ "profile",
3668
+ "email"
3669
+ ] }],
3670
+ parameters: [{
3671
+ name: "Authorization",
3672
+ in: "header",
3673
+ required: false,
3674
+ schema: { type: "string" },
3675
+ description: "Bearer or DPoP access token"
3676
+ }, {
3677
+ name: "DPoP",
3678
+ in: "header",
3679
+ required: false,
3680
+ schema: { type: "string" },
3681
+ description: "RFC 9449 DPoP proof JWT when using a DPoP-bound access token"
3682
+ }],
3683
+ responses: {
3684
+ "200": {
3685
+ description: "User information retrieved successfully",
3686
+ content: { "application/json": { schema: {
3687
+ type: "object",
3688
+ properties: {
3689
+ sub: {
3690
+ type: "string",
3691
+ description: "Subject identifier (user ID)"
3692
+ },
3693
+ email: {
3694
+ type: "string",
3695
+ format: "email",
3696
+ nullable: true,
3697
+ description: "User's email address, included if 'email' scope is granted"
3698
+ },
3699
+ name: {
3700
+ type: "string",
3701
+ nullable: true,
3702
+ description: "User's full name, included if 'profile' scope is granted"
3703
+ },
3704
+ picture: {
3705
+ type: "string",
3706
+ format: "uri",
3707
+ nullable: true,
3708
+ description: "User's profile picture URL, included if 'profile' scope is granted"
3709
+ },
3710
+ given_name: {
3711
+ type: "string",
3712
+ nullable: true,
3713
+ description: "User's given name, included if 'profile' scope is granted"
3714
+ },
3715
+ family_name: {
3716
+ type: "string",
3717
+ nullable: true,
3718
+ description: "User's family name, included if 'profile' scope is granted"
3719
+ },
3720
+ email_verified: {
3721
+ type: "boolean",
3722
+ nullable: true,
3723
+ description: "Whether the email is verified, included if 'email' scope is granted"
3724
+ }
4031
3725
  },
4032
- given_name: {
4033
- type: "string",
4034
- nullable: true,
4035
- description: "User's given name, included if 'profile' scope is granted"
3726
+ required: ["sub"]
3727
+ } } }
3728
+ },
3729
+ "401": {
3730
+ description: "Unauthorized - invalid or missing access token",
3731
+ content: { "application/json": { schema: {
3732
+ type: "object",
3733
+ properties: {
3734
+ error: { type: "string" },
3735
+ error_description: { type: "string" }
4036
3736
  },
4037
- family_name: {
4038
- type: "string",
4039
- nullable: true,
4040
- description: "User's family name, included if 'profile' scope is granted"
3737
+ required: ["error"]
3738
+ } } }
3739
+ },
3740
+ "403": {
3741
+ description: "Forbidden - insufficient scope",
3742
+ content: { "application/json": { schema: {
3743
+ type: "object",
3744
+ properties: {
3745
+ error: { type: "string" },
3746
+ error_description: { type: "string" }
4041
3747
  },
4042
- email_verified: {
4043
- type: "boolean",
4044
- nullable: true,
4045
- description: "Whether the email is verified, included if 'email' scope is granted"
4046
- }
4047
- },
4048
- required: ["sub"]
4049
- } } }
4050
- },
4051
- "401": {
4052
- description: "Unauthorized - invalid or missing access token",
4053
- content: { "application/json": { schema: {
4054
- type: "object",
4055
- properties: {
4056
- error: { type: "string" },
4057
- error_description: { type: "string" }
4058
- },
4059
- required: ["error"]
4060
- } } }
4061
- },
4062
- "403": {
4063
- description: "Forbidden - insufficient scope",
4064
- content: { "application/json": { schema: {
4065
- type: "object",
4066
- properties: {
4067
- error: { type: "string" },
4068
- error_description: { type: "string" }
4069
- },
4070
- required: ["error"]
4071
- } } }
3748
+ required: ["error"]
3749
+ } } }
3750
+ }
4072
3751
  }
4073
3752
  }
4074
- } }
3753
+ }
4075
3754
  }, async (ctx) => {
4076
3755
  return userInfoEndpoint(ctx, opts);
4077
3756
  }),
@@ -4108,194 +3787,149 @@ const oauthProvider = (options) => {
4108
3787
  }),
4109
3788
  registerOAuthClient: createOAuthEndpoint("/oauth2/register", {
4110
3789
  method: "POST",
4111
- body: z.object({
4112
- redirect_uris: z.array(SafeUrlSchema).min(1).min(1),
4113
- scope: z.string().optional(),
4114
- client_name: z.string().optional(),
4115
- client_uri: z.string().optional(),
4116
- logo_uri: z.string().optional(),
4117
- contacts: z.array(z.string().min(1)).min(1).optional(),
4118
- tos_uri: z.string().optional(),
4119
- policy_uri: z.string().optional(),
4120
- software_id: z.string().optional(),
4121
- software_version: z.string().optional(),
4122
- software_statement: z.string().optional(),
4123
- post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
4124
- backchannel_logout_uri: SafeUrlSchema.optional(),
4125
- backchannel_logout_session_required: z.boolean().optional(),
4126
- token_endpoint_auth_method: z.enum([
4127
- "none",
4128
- "client_secret_basic",
4129
- "client_secret_post",
4130
- "private_key_jwt"
4131
- ]).default("client_secret_basic").optional(),
4132
- 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(),
4133
- jwks_uri: z.string().optional(),
4134
- grant_types: z.array(z.enum([
4135
- "authorization_code",
4136
- "client_credentials",
4137
- "refresh_token"
4138
- ])).default(["authorization_code"]).optional(),
4139
- response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
4140
- type: z.enum([
4141
- "web",
4142
- "native",
4143
- "user-agent-based"
4144
- ]).optional(),
4145
- subject_type: z.enum(["public", "pairwise"]).optional(),
4146
- skip_consent: z.never({ error: "skip_consent cannot be set during dynamic client registration" }).optional()
4147
- }),
3790
+ body: clientRegistrationRequestSchema,
4148
3791
  errorCodesByField: {
4149
3792
  redirect_uris: "invalid_redirect_uri",
4150
3793
  post_logout_redirect_uris: "invalid_redirect_uri",
4151
- software_statement: "invalid_software_statement"
3794
+ software_statement: "invalid_software_statement",
3795
+ resources: "invalid_target"
4152
3796
  },
4153
3797
  defaultError: "invalid_client_metadata",
4154
- metadata: { openapi: {
4155
- description: "Register an OAuth2 application",
4156
- responses: { "200": {
4157
- description: "OAuth2 application registered successfully",
4158
- content: { "application/json": { schema: {
4159
- type: "object",
4160
- properties: {
4161
- client_id: {
4162
- type: "string",
4163
- description: "Unique identifier for the client"
4164
- },
4165
- client_secret: {
4166
- type: "string",
4167
- description: "Secret key for the client"
4168
- },
4169
- client_secret_expires_at: {
4170
- type: "number",
4171
- description: "Time the client secret will expire. If 0, the client secret will never expire."
4172
- },
4173
- scope: {
4174
- type: "string",
4175
- description: "Space-separated scopes allowed by the client"
4176
- },
4177
- user_id: {
4178
- type: "string",
4179
- description: "ID of the user who registered the client, null if registered anonymously"
4180
- },
4181
- client_id_issued_at: {
4182
- type: "number",
4183
- description: "Creation timestamp of this client"
4184
- },
4185
- client_name: {
4186
- type: "string",
4187
- description: "Name of the OAuth2 application"
4188
- },
4189
- client_uri: {
4190
- type: "string",
4191
- description: "Name of the OAuth2 application"
4192
- },
4193
- logo_uri: {
4194
- type: "string",
4195
- description: "Icon URL for the application"
4196
- },
4197
- contacts: {
4198
- type: "array",
4199
- items: { type: "string" },
4200
- description: "List representing ways to contact people responsible for this client, typically email addresses"
4201
- },
4202
- tos_uri: {
4203
- type: "string",
4204
- description: "Client's terms of service uri"
4205
- },
4206
- policy_uri: {
4207
- type: "string",
4208
- description: "Client's policy uri"
4209
- },
4210
- software_id: {
4211
- type: "string",
4212
- description: "Unique identifier assigned by the developer to help in the dynamic registration process"
4213
- },
4214
- software_version: {
4215
- type: "string",
4216
- description: "Version identifier for the software_id"
4217
- },
4218
- software_statement: {
4219
- type: "string",
4220
- description: "JWT containing metadata values about the client software as claims"
4221
- },
4222
- redirect_uris: {
4223
- type: "array",
4224
- items: {
3798
+ metadata: {
3799
+ noStore: true,
3800
+ openapi: {
3801
+ description: "Register an OAuth2 application",
3802
+ responses: { "201": {
3803
+ description: "OAuth2 application registered successfully",
3804
+ content: { "application/json": { schema: {
3805
+ type: "object",
3806
+ properties: {
3807
+ client_id: {
4225
3808
  type: "string",
4226
- format: "uri"
3809
+ description: "Unique identifier for the client"
4227
3810
  },
4228
- description: "List of allowed redirect uris"
4229
- },
4230
- post_logout_redirect_uris: {
4231
- type: "array",
4232
- items: {
3811
+ client_secret: {
4233
3812
  type: "string",
4234
- format: "uri"
3813
+ description: "Secret key for the client"
4235
3814
  },
4236
- description: "List of allowed logout redirect uris"
4237
- },
4238
- backchannel_logout_uri: {
4239
- type: "string",
4240
- format: "uri",
4241
- description: "RP URL to receive signed Logout Tokens when the end-user's OP session terminates"
4242
- },
4243
- backchannel_logout_session_required: {
4244
- type: "boolean",
4245
- description: "Whether the RP requires a `sid` claim in every Logout Token"
4246
- },
4247
- token_endpoint_auth_method: {
4248
- type: "string",
4249
- description: "Requested authentication method for the token endpoint",
4250
- enum: [
4251
- "none",
4252
- "client_secret_basic",
4253
- "client_secret_post",
4254
- "private_key_jwt"
4255
- ]
4256
- },
4257
- grant_types: {
4258
- type: "array",
4259
- items: {
3815
+ client_secret_expires_at: {
3816
+ type: "number",
3817
+ description: "Time the client secret will expire. If 0, the client secret will never expire."
3818
+ },
3819
+ scope: {
4260
3820
  type: "string",
4261
- enum: [
4262
- "authorization_code",
4263
- "client_credentials",
4264
- "refresh_token"
4265
- ]
3821
+ description: "Space-separated scopes allowed by the client"
4266
3822
  },
4267
- description: "Requested authentication method for the token endpoint"
4268
- },
4269
- response_types: {
4270
- type: "array",
4271
- items: {
3823
+ user_id: {
4272
3824
  type: "string",
4273
- enum: ["code"]
3825
+ description: "ID of the user who registered the client, null if registered anonymously"
4274
3826
  },
4275
- description: "Requested authentication method for the token endpoint"
4276
- },
4277
- public: {
4278
- type: "boolean",
4279
- description: "Whether the client is public as determined by the type"
4280
- },
4281
- type: {
4282
- type: "string",
4283
- description: "Type of the client",
4284
- enum: [
4285
- "web",
4286
- "native",
4287
- "user-agent-based"
4288
- ]
3827
+ client_id_issued_at: {
3828
+ type: "number",
3829
+ description: "Creation timestamp of this client"
3830
+ },
3831
+ client_name: {
3832
+ type: "string",
3833
+ description: "Name of the OAuth2 application"
3834
+ },
3835
+ client_uri: {
3836
+ type: "string",
3837
+ description: "Name of the OAuth2 application"
3838
+ },
3839
+ logo_uri: {
3840
+ type: "string",
3841
+ description: "Icon URL for the application"
3842
+ },
3843
+ contacts: {
3844
+ type: "array",
3845
+ items: { type: "string" },
3846
+ description: "List representing ways to contact people responsible for this client, typically email addresses"
3847
+ },
3848
+ tos_uri: {
3849
+ type: "string",
3850
+ description: "Client's terms of service uri"
3851
+ },
3852
+ policy_uri: {
3853
+ type: "string",
3854
+ description: "Client's policy uri"
3855
+ },
3856
+ software_id: {
3857
+ type: "string",
3858
+ description: "Unique identifier assigned by the developer to help in the dynamic registration process"
3859
+ },
3860
+ software_version: {
3861
+ type: "string",
3862
+ description: "Version identifier for the software_id"
3863
+ },
3864
+ software_statement: {
3865
+ type: "string",
3866
+ description: "JWT containing metadata values about the client software as claims"
3867
+ },
3868
+ redirect_uris: {
3869
+ type: "array",
3870
+ items: {
3871
+ type: "string",
3872
+ format: "uri"
3873
+ },
3874
+ description: "List of allowed redirect uris"
3875
+ },
3876
+ post_logout_redirect_uris: {
3877
+ type: "array",
3878
+ items: {
3879
+ type: "string",
3880
+ format: "uri"
3881
+ },
3882
+ description: "List of allowed logout redirect uris"
3883
+ },
3884
+ backchannel_logout_uri: {
3885
+ type: "string",
3886
+ format: "uri",
3887
+ description: "RP URL to receive signed Logout Tokens when the end-user's OP session terminates"
3888
+ },
3889
+ backchannel_logout_session_required: {
3890
+ type: "boolean",
3891
+ description: "Whether the RP requires a `sid` claim in every Logout Token"
3892
+ },
3893
+ token_endpoint_auth_method: {
3894
+ type: "string",
3895
+ description: "Requested authentication method for the token endpoint"
3896
+ },
3897
+ grant_types: {
3898
+ type: "array",
3899
+ items: { type: "string" },
3900
+ description: "Grant types the client may use at the token endpoint"
3901
+ },
3902
+ response_types: {
3903
+ type: "array",
3904
+ items: {
3905
+ type: "string",
3906
+ enum: ["code"]
3907
+ },
3908
+ description: "Response types the client may use at the authorization endpoint"
3909
+ },
3910
+ public: {
3911
+ type: "boolean",
3912
+ description: "Whether the client is public as determined by the type"
3913
+ },
3914
+ type: {
3915
+ type: "string",
3916
+ description: "Type of the client",
3917
+ enum: [
3918
+ "web",
3919
+ "native",
3920
+ "user-agent-based"
3921
+ ]
3922
+ },
3923
+ disabled: {
3924
+ type: "boolean",
3925
+ description: "Whether the client is disabled"
3926
+ }
4289
3927
  },
4290
- disabled: {
4291
- type: "boolean",
4292
- description: "Whether the client is disabled"
4293
- }
4294
- },
4295
- required: ["client_id"]
4296
- } } }
4297
- } }
4298
- } }
3928
+ required: ["client_id"]
3929
+ } } }
3930
+ } }
3931
+ }
3932
+ }
4299
3933
  }, async (ctx) => {
4300
3934
  return registerEndpoint(ctx, opts);
4301
3935
  }),
@@ -4312,7 +3946,14 @@ const oauthProvider = (options) => {
4312
3946
  getOAuthConsent: getOAuthConsent(opts),
4313
3947
  getOAuthConsents: getOAuthConsents(opts),
4314
3948
  updateOAuthConsent: updateOAuthConsent(opts),
4315
- deleteOAuthConsent: deleteOAuthConsent(opts)
3949
+ deleteOAuthConsent: deleteOAuthConsent(opts),
3950
+ adminCreateOAuthResource: adminCreateOAuthResource(opts),
3951
+ adminListOAuthResources: adminListOAuthResources(opts),
3952
+ adminGetOAuthResource: adminGetOAuthResource(opts),
3953
+ adminUpdateOAuthResource: adminUpdateOAuthResource(opts),
3954
+ adminDeleteOAuthResource: adminDeleteOAuthResource(opts),
3955
+ adminLinkClientResource: adminLinkClientResource(opts),
3956
+ adminUnlinkClientResource: adminUnlinkClientResource(opts)
4316
3957
  },
4317
3958
  schema: mergeSchema(schema, opts?.schema),
4318
3959
  rateLimit: [
@@ -4558,6 +4199,20 @@ async function authorizeEndpoint(ctx, opts, settings) {
4558
4199
  requestedScopes = client.scopes ?? opts.scopes ?? [];
4559
4200
  query.scope = requestedScopes.join(" ");
4560
4201
  }
4202
+ if (query.resource !== void 0) try {
4203
+ await resolveResourcePolicy(ctx, opts, {
4204
+ resource: query.resource,
4205
+ clientId: client.clientId,
4206
+ requestedScopes
4207
+ });
4208
+ } catch (err) {
4209
+ if (err instanceof APIError$1) {
4210
+ const error = err.body?.error ?? "invalid_target";
4211
+ const description = err.body?.error_description ?? "requested resource invalid";
4212
+ return handleRedirect(ctx, formatErrorURL(query.redirect_uri, error, description, query.state, getIssuer(ctx, opts)));
4213
+ }
4214
+ throw err;
4215
+ }
4561
4216
  const pkceRequired = isPKCERequired(client, requestedScopes);
4562
4217
  if (pkceRequired) {
4563
4218
  if (!query.code_challenge || !query.code_challenge_method) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", pkceRequired.valueOf(), query.state, getIssuer(ctx, opts)));
@@ -4566,9 +4221,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
4566
4221
  if (!query.code_challenge || !query.code_challenge_method) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", "code_challenge and code_challenge_method must both be provided", query.state, getIssuer(ctx, opts)));
4567
4222
  if (!["S256"].includes(query.code_challenge_method)) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method, only S256 is supported", query.state, getIssuer(ctx, opts)));
4568
4223
  }
4569
- const resource = query.resource;
4570
- if (!(await checkResource(ctx, opts, resource, requestedScopes)).success) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_target", "requested resource invalid", query.state, getIssuer(ctx, opts)));
4571
- const requestedResources = toResourceList(resource) ?? [];
4224
+ const requestedResources = toResourceList(query.resource) ?? [];
4572
4225
  const session = await getSessionFromCtx(ctx);
4573
4226
  const maxAgeSeconds = query.max_age;
4574
4227
  const hasSatisfiedMaxAge = session != null && maxAgeSeconds !== void 0 && isWithinMaxAge(new Date(session.session.createdAt), maxAgeSeconds, /* @__PURE__ */ new Date());
@@ -4720,126 +4373,13 @@ async function signParams(ctx, opts, flags) {
4720
4373
  const params = serializeAuthorizationQuery(ctx.query);
4721
4374
  params.set("exp", String(exp));
4722
4375
  params.set(signedQueryIssuedAtParam, String(issuedAt));
4376
+ params.delete("sig");
4723
4377
  params.delete(postLoginClearedParam);
4724
4378
  if (flags?.postLoginClearedForSession) params.set(postLoginClearedParam, flags.postLoginClearedForSession);
4725
- const signature = await makeSignature(params.toString(), ctx.context.secret);
4726
- params.append("sig", signature);
4379
+ setSignedOAuthQueryParameterNames(params);
4380
+ const signature = await makeSignature(canonicalizeOAuthQueryParams(params).toString(), ctx.context.secret);
4381
+ params.set("sig", signature);
4727
4382
  return params.toString();
4728
4383
  }
4729
4384
  //#endregion
4730
- //#region src/metadata.ts
4731
- function authServerMetadata(ctx, opts, overrides) {
4732
- const baseURL = ctx.context.baseURL;
4733
- const backchannelSupported = !overrides?.jwt_disabled;
4734
- return {
4735
- scopes_supported: overrides?.scopes_supported,
4736
- issuer: validateIssuerUrl(opts?.jwt?.issuer ?? baseURL),
4737
- authorization_endpoint: `${baseURL}/oauth2/authorize`,
4738
- token_endpoint: `${baseURL}/oauth2/token`,
4739
- jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
4740
- registration_endpoint: overrides?.dynamic_client_registration_supported ? `${baseURL}/oauth2/register` : void 0,
4741
- introspection_endpoint: `${baseURL}/oauth2/introspect`,
4742
- revocation_endpoint: `${baseURL}/oauth2/revoke`,
4743
- response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
4744
- response_modes_supported: ["query"],
4745
- grant_types_supported: overrides?.grant_types_supported ?? [
4746
- "authorization_code",
4747
- "client_credentials",
4748
- "refresh_token"
4749
- ],
4750
- token_endpoint_auth_methods_supported: [
4751
- ...overrides?.public_client_supported ? ["none"] : [],
4752
- "client_secret_basic",
4753
- "client_secret_post",
4754
- "private_key_jwt"
4755
- ],
4756
- token_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
4757
- introspection_endpoint_auth_methods_supported: [
4758
- "client_secret_basic",
4759
- "client_secret_post",
4760
- "private_key_jwt"
4761
- ],
4762
- introspection_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
4763
- revocation_endpoint_auth_methods_supported: [
4764
- "client_secret_basic",
4765
- "client_secret_post",
4766
- "private_key_jwt"
4767
- ],
4768
- revocation_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
4769
- code_challenge_methods_supported: ["S256"],
4770
- authorization_response_iss_parameter_supported: true,
4771
- backchannel_logout_supported: backchannelSupported,
4772
- backchannel_logout_session_supported: backchannelSupported
4773
- };
4774
- }
4775
- function oidcServerMetadata(ctx, opts) {
4776
- const baseURL = ctx.context.baseURL;
4777
- const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
4778
- return {
4779
- ...authServerMetadata(ctx, jwtPluginOptions, {
4780
- scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
4781
- dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
4782
- public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
4783
- grant_types_supported: opts.grantTypes,
4784
- jwt_disabled: opts.disableJwtPlugin
4785
- }),
4786
- claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
4787
- userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
4788
- subject_types_supported: opts.pairwiseSecret ? ["public", "pairwise"] : ["public"],
4789
- id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
4790
- end_session_endpoint: `${baseURL}/oauth2/end-session`,
4791
- acr_values_supported: ["urn:mace:incommon:iap:bronze"],
4792
- prompt_values_supported: [
4793
- "login",
4794
- "consent",
4795
- "create",
4796
- "select_account",
4797
- "none"
4798
- ],
4799
- ...mergeDiscoveryMetadata(opts.clientDiscovery)
4800
- };
4801
- }
4802
- const METADATA_CACHE_CONTROL = "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400";
4803
- function metadataResponse(body, extraHeaders) {
4804
- const headers = new Headers(extraHeaders);
4805
- if (!headers.has("Cache-Control")) headers.set("Cache-Control", METADATA_CACHE_CONTROL);
4806
- headers.set("Content-Type", "application/json");
4807
- return new Response(JSON.stringify(body), {
4808
- status: 200,
4809
- headers
4810
- });
4811
- }
4812
- /**
4813
- * Provides an exportable `/.well-known/oauth-authorization-server`.
4814
- *
4815
- * Useful when basePath prevents the endpoint from being located at the root
4816
- * and must be provided manually.
4817
- *
4818
- * @external
4819
- */
4820
- const oauthProviderAuthServerMetadata = (auth, opts) => {
4821
- return async (request) => {
4822
- return metadataResponse(await auth.api.getOAuthServerConfig({
4823
- request,
4824
- asResponse: false
4825
- }), opts?.headers);
4826
- };
4827
- };
4828
- /**
4829
- * Provides an exportable `/.well-known/openid-configuration`.
4830
- *
4831
- * Useful when basePath prevents the endpoint from being located at the root
4832
- * and must be provided manually.
4833
- *
4834
- * @external
4835
- */
4836
- const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
4837
- return async (request) => {
4838
- return metadataResponse(await auth.api.getOpenIdConfig({
4839
- request,
4840
- asResponse: false
4841
- }), opts?.headers);
4842
- };
4843
- };
4844
- //#endregion
4845
- export { authServerMetadata, checkOAuthClient, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata };
4385
+ export { DEFAULT_OAUTH_SCOPES, ResourceUriSchema, authServerMetadata, checkOAuthClient, consumeClientAssertion, extendOAuthProvider, getIssuer, getOAuthProviderApi, getOAuthProviderState, metadataResponse, oauthAuthorizationServerMetadata, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata, raiseResourceServerChallenge };