@better-auth/oauth-provider 1.7.0-beta.3 → 1.7.0-beta.4

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,11 +1,11 @@
1
- import { n as isPrivateHostname } from "./client-assertion-BYtMWGCE.mjs";
1
+ import { n as isPrivateHostname } from "./client-assertion-DLMKVgoj.mjs";
2
2
  import { n as mcpHandler } from "./mcp-CYnz-MXn.mjs";
3
- import { C as validateClientCredentials, S as toClientDiscoveryArray, _ as resolveSubjectIdentifier, a as getJwtPlugin, b as storeClientSecret, c as getStoredToken, d as normalizeTimestampValue, f as parseClientMetadata, g as resolveSessionAuthTime, h as removePromptFromQuery, i as getClient, l as isPKCERequired, m as postLoginClearedParam, n as destructureCredentials, p as parsePrompt, r as extractClientCredentials, s as getSignedQueryIssuedAt, t as decryptStoredClientSecret, u as mergeDiscoveryMetadata, v as searchParamsToQuery, w as verifyOAuthQueryParams, x as storeToken, y as signedQueryIssuedAtParam } from "./utils-_Jr_enAe.mjs";
4
- import { t as PACKAGE_VERSION } from "./version-CG1YnCiF.mjs";
3
+ import { C as toAudienceClaim, D as verifyOAuthQueryParams, E as validateClientCredentials, S as storeToken, T as toResourceList, _ as resolveSessionAuthTime, a as getClient, b as signedQueryIssuedAtParam, c as getSignedQueryIssuedAt, d as mergeDiscoveryMetadata, f as normalizeTimestampValue, g as removePromptFromQuery, h as postLoginClearedParam, i as extractClientCredentials, l as getStoredToken, m as parsePrompt, n as decryptStoredClientSecret, o as getJwtPlugin, p as parseClientMetadata, r as destructureCredentials, t as checkResource, u as isPKCERequired, v as resolveSubjectIdentifier, w as toClientDiscoveryArray, x as storeClientSecret, y as searchParamsToQuery } from "./utils-DKBWQ8fe.mjs";
4
+ import { t as PACKAGE_VERSION } from "./version-nFnRm-a3.mjs";
5
5
  import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
6
6
  import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
7
7
  import { APIError as APIError$1 } from "better-call";
8
- import { ASSERTION_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
8
+ import { PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
9
9
  import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
10
10
  import { isLoopbackHost, isLoopbackIP } from "@better-auth/core/utils/host";
11
11
  import { generateRandomString, makeSignature } from "better-auth/crypto";
@@ -17,6 +17,7 @@ import { mergeSchema } from "better-auth/db";
17
17
  import * as z from "zod";
18
18
  import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
19
19
  import { SignJWT, base64url, compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader } from "jose";
20
+ import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
20
21
  //#region src/consent.ts
21
22
  async function consentEndpoint(ctx, opts) {
22
23
  const oauthRequest = await oAuthState.get();
@@ -78,12 +79,14 @@ async function consentEndpoint(ctx, opts) {
78
79
  ]
79
80
  });
80
81
  const iat = Math.floor(Date.now() / 1e3);
82
+ const resource = query.getAll("resource");
81
83
  const consent = {
82
84
  clientId,
83
85
  userId: session?.user.id,
84
86
  scopes: requestedScopes ?? originalRequestedScopes,
85
87
  createdAt: /* @__PURE__ */ new Date(iat * 1e3),
86
88
  updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
89
+ resources: resource.length ? resource : void 0,
87
90
  referenceId
88
91
  };
89
92
  foundConsent?.id ? await ctx.context.adapter.update({
@@ -93,6 +96,7 @@ async function consentEndpoint(ctx, opts) {
93
96
  value: foundConsent.id
94
97
  }],
95
98
  update: {
99
+ resources: consent.resources,
96
100
  scopes: consent.scopes,
97
101
  updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
98
102
  }
@@ -177,11 +181,6 @@ async function postLogin(ctx, opts) {
177
181
  }
178
182
  //#endregion
179
183
  //#region src/types/zod.ts
180
- const DANGEROUS_SCHEMES = [
181
- "javascript:",
182
- "data:",
183
- "vbscript:"
184
- ];
185
184
  /**
186
185
  * Runtime schema for OAuthAuthorizationQuery.
187
186
  * Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
@@ -202,7 +201,8 @@ const oauthAuthorizationQuerySchema = z.object({
202
201
  id_token_hint: z.string().optional(),
203
202
  code_challenge: z.string().optional(),
204
203
  code_challenge_method: z.literal("S256").optional(),
205
- nonce: z.string().optional()
204
+ nonce: z.string().optional(),
205
+ resource: z.union([z.string(), z.array(z.string())]).optional()
206
206
  }).passthrough();
207
207
  /**
208
208
  * Runtime schema for the authorization code verification value.
@@ -215,34 +215,40 @@ const verificationValueSchema = z.object({
215
215
  sessionId: z.string(),
216
216
  userId: z.string(),
217
217
  referenceId: z.string().optional(),
218
- authTime: z.number().optional()
218
+ authTime: z.number().optional(),
219
+ resource: z.array(z.string()).optional()
219
220
  }).passthrough();
221
+ const DANGEROUS_SCHEMES = [
222
+ "javascript:",
223
+ "data:",
224
+ "vbscript:"
225
+ ];
220
226
  /**
221
- * Reusable URL validation for OAuth redirect URIs.
222
- * - Blocks dangerous schemes (javascript:, data:, vbscript:)
223
- * - For http/https: requires HTTPS (HTTP allowed only for loopback hosts: 127.0.0.0/8, [::1], *.localhost per RFC 6761)
224
- * - Allows custom schemes for mobile apps (e.g., myapp://callback)
227
+ * Validates an RFC 8707 resource indicator. The value must be an absolute URI
228
+ * with no fragment (RFC 8707 §2). Unlike a redirect URI it is not restricted to
229
+ * HTTPS, because a resource server identifier may use any absolute URI scheme;
230
+ * the configured `validAudiences` allowlist is the authoritative control over
231
+ * which resources a token may target.
225
232
  */
226
- const SafeUrlSchema = z.url().superRefine((val, ctx) => {
233
+ const ResourceUriSchema = z.string().superRefine((val, ctx) => {
227
234
  if (!URL.canParse(val)) {
228
235
  ctx.addIssue({
229
236
  code: "custom",
230
- message: "URL must be parseable",
237
+ message: "resource must be an absolute URI",
231
238
  fatal: true
232
239
  });
233
240
  return z.NEVER;
234
241
  }
235
- const u = new URL(val);
236
- if (DANGEROUS_SCHEMES.includes(u.protocol)) {
242
+ if (val.includes("#")) {
237
243
  ctx.addIssue({
238
244
  code: "custom",
239
- message: "URL cannot use javascript:, data:, or vbscript: scheme"
245
+ message: "resource must not contain a fragment"
240
246
  });
241
247
  return;
242
248
  }
243
- if (u.protocol === "http:" && !isLoopbackHost(u.host)) ctx.addIssue({
249
+ if (DANGEROUS_SCHEMES.includes(new URL(val).protocol)) ctx.addIssue({
244
250
  code: "custom",
245
- message: "Redirect URI must use HTTPS (HTTP allowed only for loopback hosts)"
251
+ message: "resource cannot use javascript:, data:, or vbscript: scheme"
246
252
  });
247
253
  });
248
254
  //#endregion
@@ -331,13 +337,13 @@ async function tokenEndpoint(ctx, opts) {
331
337
  case "refresh_token": return handleRefreshTokenGrant(ctx, opts);
332
338
  }
333
339
  }
334
- async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, overrides) {
340
+ async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, resources, referenceId, overrides) {
335
341
  const iat = overrides?.iat ?? Math.floor(Date.now() / 1e3);
336
342
  const exp = overrides?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
337
343
  const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
338
344
  user,
339
345
  scopes,
340
- resource: ctx.body.resource,
346
+ resources,
341
347
  referenceId,
342
348
  metadata: parseClientMetadata(client.metadata)
343
349
  }) : {};
@@ -347,7 +353,7 @@ async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, r
347
353
  payload: {
348
354
  ...customClaims,
349
355
  sub: user?.id,
350
- aud: typeof audience === "string" ? audience : audience?.length === 1 ? audience.at(0) : audience,
356
+ aud: toAudienceClaim(audience),
351
357
  azp: client.clientId,
352
358
  scope: scopes.join(" "),
353
359
  sid: overrides?.sid,
@@ -438,7 +444,7 @@ async function decodeRefreshToken(opts, token) {
438
444
  });
439
445
  return opts.formatRefreshToken?.decrypt ? opts.formatRefreshToken?.decrypt(token) : { token };
440
446
  }
441
- async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, referenceId, refreshId) {
447
+ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, refreshId) {
442
448
  const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
443
449
  const exp = payload?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
444
450
  const token = opts.generateOpaqueAccessToken ? await opts.generateOpaqueAccessToken() : generateRandomString(32, "A-Z", "a-z");
@@ -450,6 +456,7 @@ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload,
450
456
  sessionId: payload?.sid,
451
457
  userId: user?.id,
452
458
  referenceId,
459
+ resources,
453
460
  refreshId,
454
461
  scopes,
455
462
  createdAt: /* @__PURE__ */ new Date(iat * 1e3),
@@ -458,54 +465,100 @@ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload,
458
465
  });
459
466
  return (opts.prefix?.opaqueAccessToken ?? "") + token;
460
467
  }
461
- async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, payload, originalRefresh, authTime) {
468
+ /**
469
+ * Tear down the entire refresh-token family for a (client, user) pair, plus
470
+ * any access tokens that reference those refresh rows, per RFC 9700 §4.14.
471
+ * Access tokens are deleted first so the parent rows' foreign-key children
472
+ * do not block the refresh-row delete.
473
+ *
474
+ * TODO(invalidate-family-race): the two `deleteMany` calls are not atomic
475
+ * with respect to each other. Between them, a concurrent rotation in a
476
+ * different worker can `create` a fresh refresh row (and, immediately after,
477
+ * an access-token row referencing it) for the same (client, user) pair,
478
+ * leaving the family partially rebuilt and the new refresh row orphaned of
479
+ * any deletion. Closing this window requires the same transactional adapter
480
+ * contract tracked under FIXME(strict-family-invalidation) in
481
+ * `createRefreshToken`.
482
+ *
483
+ * @internal
484
+ */
485
+ async function invalidateRefreshFamily(ctx, clientId, userId) {
486
+ const refreshTokens = await ctx.context.adapter.findMany({
487
+ model: "oauthRefreshToken",
488
+ where: [{
489
+ field: "clientId",
490
+ value: clientId
491
+ }, {
492
+ field: "userId",
493
+ value: userId
494
+ }]
495
+ });
496
+ if (refreshTokens.length) await ctx.context.adapter.deleteMany({
497
+ model: "oauthAccessToken",
498
+ where: [{
499
+ field: "refreshId",
500
+ operator: "in",
501
+ value: refreshTokens.map((r) => r.id)
502
+ }]
503
+ });
504
+ await ctx.context.adapter.deleteMany({
505
+ model: "oauthRefreshToken",
506
+ where: [{
507
+ field: "clientId",
508
+ value: clientId
509
+ }, {
510
+ field: "userId",
511
+ value: userId
512
+ }]
513
+ });
514
+ }
515
+ async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, payload, originalRefresh, authTime, resources) {
462
516
  const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
463
517
  const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
464
518
  const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
465
519
  const sessionId = payload?.sid;
466
- if (originalRefresh?.id) await ctx.context.adapter.update({
520
+ const newRow = {
521
+ token: await storeToken(opts.storeTokens, token, "refresh_token"),
522
+ clientId: client.clientId,
523
+ sessionId,
524
+ userId: user.id,
525
+ referenceId,
526
+ authTime,
527
+ scopes,
528
+ resources,
529
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3),
530
+ expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
531
+ };
532
+ if (!originalRefresh?.id) return {
533
+ id: (await ctx.context.adapter.create({
534
+ model: "oauthRefreshToken",
535
+ data: newRow
536
+ })).id,
537
+ token: await encodeRefreshToken(opts, token, sessionId)
538
+ };
539
+ if (!await ctx.context.adapter.update({
467
540
  model: "oauthRefreshToken",
468
541
  where: [{
469
542
  field: "id",
470
543
  value: originalRefresh.id
544
+ }, {
545
+ field: "revoked",
546
+ operator: "eq",
547
+ value: null
471
548
  }],
472
549
  update: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
550
+ })) throw new APIError("BAD_REQUEST", {
551
+ error_description: "invalid refresh token",
552
+ error: "invalid_grant"
473
553
  });
474
554
  return {
475
555
  id: (await ctx.context.adapter.create({
476
556
  model: "oauthRefreshToken",
477
- data: {
478
- token: await storeToken(opts.storeTokens, token, "refresh_token"),
479
- clientId: client.clientId,
480
- sessionId,
481
- userId: user.id,
482
- referenceId,
483
- authTime,
484
- scopes,
485
- createdAt: /* @__PURE__ */ new Date(iat * 1e3),
486
- expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
487
- }
557
+ data: newRow
488
558
  })).id,
489
559
  token: await encodeRefreshToken(opts, token, sessionId)
490
560
  };
491
561
  }
492
- /**
493
- * Checks the resource parameter, if provided,
494
- * and returns a valid audience based on the request
495
- */
496
- async function checkResource(ctx, opts, scopes) {
497
- const resource = ctx.body.resource;
498
- const audience = typeof resource === "string" ? [resource] : resource ? [...resource] : void 0;
499
- if (audience) {
500
- if (scopes.includes("openid")) audience.push(`${ctx.context.baseURL}/oauth2/userinfo`);
501
- const validAudiences = new Set([...opts.validAudiences ?? [ctx.context.baseURL], scopes?.includes("openid") ? `${ctx.context.baseURL}/oauth2/userinfo` : void 0].flat().filter((v) => v?.length));
502
- for (const aud of audience) if (!validAudiences.has(aud)) throw new APIError("BAD_REQUEST", {
503
- error_description: "requested resource invalid",
504
- error: "invalid_request"
505
- });
506
- }
507
- return audience?.length === 1 ? audience.at(0) : audience;
508
- }
509
562
  async function createUserTokens(ctx, opts, params) {
510
563
  const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, verificationValue } = params;
511
564
  const iat = Math.floor(Date.now() / 1e3);
@@ -513,7 +566,12 @@ async function createUserTokens(ctx, opts, params) {
513
566
  const exp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
514
567
  return prev < curr ? prev : curr;
515
568
  }, defaultExp) : defaultExp;
516
- const audience = await checkResource(ctx, opts, scopes);
569
+ const resourceResult = await checkResource(ctx, opts, params?.resources, scopes);
570
+ if (!resourceResult.success) throw new APIError("BAD_REQUEST", {
571
+ error_description: "requested resource invalid",
572
+ error: "invalid_target"
573
+ });
574
+ const audience = resourceResult.audience;
517
575
  const isRefreshToken = user && (existingRefreshToken?.scopes?.includes("offline_access") || scopes.includes("offline_access"));
518
576
  const isJwtAccessToken = audience && !opts.disableJwtPlugin;
519
577
  const isIdToken = user && scopes.includes("openid");
@@ -524,12 +582,13 @@ async function createUserTokens(ctx, opts, params) {
524
582
  metadata: parseClientMetadata(client.metadata),
525
583
  verificationValue
526
584
  }) : void 0;
585
+ const refreshResources = params?.refreshToken?.resources ?? params?.originalResources ?? params?.resources;
527
586
  const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
528
587
  iat,
529
588
  exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
530
589
  sid: sessionId
531
- }, existingRefreshToken, authTime) : void 0;
532
- const [accessToken, refreshToken] = await Promise.all([isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, {
590
+ }, existingRefreshToken, authTime, refreshResources) : void 0;
591
+ const [accessToken, refreshToken] = await Promise.all([isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, params?.resources, referenceId, {
533
592
  iat,
534
593
  exp,
535
594
  sid: sessionId
@@ -537,11 +596,11 @@ async function createUserTokens(ctx, opts, params) {
537
596
  iat,
538
597
  exp,
539
598
  sid: sessionId
540
- }, referenceId, earlyRefreshToken?.id), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
599
+ }, params?.resources, referenceId, earlyRefreshToken?.id), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
541
600
  iat,
542
601
  exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
543
602
  sid: sessionId
544
- }, existingRefreshToken, authTime) : void 0]);
603
+ }, existingRefreshToken, authTime, refreshResources) : void 0]);
545
604
  const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) : void 0;
546
605
  return ctx.json({
547
606
  ...customFields,
@@ -558,16 +617,11 @@ async function createUserTokens(ctx, opts, params) {
558
617
  } });
559
618
  }
560
619
  /** Checks verification value */
561
- async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri) {
562
- const verification = await ctx.context.internalAdapter.findVerificationValue(await storeToken(opts.storeTokens, code, "authorization_code"));
620
+ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resource) {
621
+ const verification = await ctx.context.internalAdapter.consumeVerificationValue(await storeToken(opts.storeTokens, code, "authorization_code"));
563
622
  if (!verification) throw new APIError("UNAUTHORIZED", {
564
- error_description: "Invalid code",
565
- error: "invalid_verification"
566
- });
567
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(await storeToken(opts.storeTokens, code, "authorization_code"));
568
- if (!verification.expiresAt || verification.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("UNAUTHORIZED", {
569
- error_description: "code expired",
570
- error: "invalid_verification"
623
+ error_description: "invalid code",
624
+ error: "invalid_grant"
571
625
  });
572
626
  let rawValue;
573
627
  try {
@@ -575,13 +629,13 @@ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri)
575
629
  } catch {
576
630
  throw new APIError("UNAUTHORIZED", {
577
631
  error_description: "malformed verification value",
578
- error: "invalid_verification"
632
+ error: "invalid_grant"
579
633
  });
580
634
  }
581
635
  const parsed = verificationValueSchema.safeParse(rawValue);
582
636
  if (!parsed.success) throw new APIError("UNAUTHORIZED", {
583
637
  error_description: "malformed verification value",
584
- error: "invalid_verification"
638
+ error: "invalid_grant"
585
639
  });
586
640
  const verificationValue = parsed.data;
587
641
  if (verificationValue.query.client_id !== client_id) throw new APIError("UNAUTHORIZED", {
@@ -592,14 +646,29 @@ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri)
592
646
  error_description: "redirect_uri mismatch",
593
647
  error: "invalid_request"
594
648
  });
595
- return verificationValue;
649
+ const storedResources = toResourceList(verificationValue.resource) ?? toResourceList(verificationValue.query.resource);
650
+ const effectiveResources = resource ?? storedResources;
651
+ if (resource && storedResources) {
652
+ const requestedSet = new Set(resource);
653
+ const authorizedSet = new Set(storedResources);
654
+ for (const r of requestedSet) if (!authorizedSet.has(r)) throw new APIError("BAD_REQUEST", {
655
+ error_description: "requested resource not authorized",
656
+ error: "invalid_target"
657
+ });
658
+ }
659
+ return {
660
+ verificationValue,
661
+ effectiveResources,
662
+ authorizedResources: storedResources
663
+ };
596
664
  }
597
665
  /**
598
666
  * Obtains new Session Jwt and Refresh Tokens using a code
599
667
  */
600
668
  async function handleAuthorizationCodeGrant(ctx, opts) {
601
669
  const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
602
- const { code, code_verifier, redirect_uri } = ctx.body;
670
+ const { code, code_verifier, redirect_uri, resource } = ctx.body;
671
+ const resources = toResourceList(resource);
603
672
  if (!client_id) throw new APIError("BAD_REQUEST", {
604
673
  error_description: "client_id is required",
605
674
  error: "invalid_request"
@@ -619,7 +688,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
619
688
  error: "invalid_request"
620
689
  });
621
690
  /** Get and check Verification Value */
622
- const verificationValue = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri);
691
+ const { verificationValue, effectiveResources, authorizedResources } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
623
692
  const scopes = verificationValue.query.scope?.split(" ");
624
693
  if (!scopes) throw new APIError("INTERNAL_SERVER_ERROR", {
625
694
  error_description: "verification scope unset",
@@ -684,7 +753,9 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
684
753
  sessionId: session.id,
685
754
  nonce: verificationValue.query?.nonce,
686
755
  authTime,
687
- verificationValue
756
+ verificationValue,
757
+ resources: effectiveResources,
758
+ originalResources: authorizedResources
688
759
  });
689
760
  }
690
761
  /**
@@ -695,7 +766,8 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
695
766
  */
696
767
  async function handleClientCredentialsGrant(ctx, opts) {
697
768
  const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
698
- const { scope } = ctx.body;
769
+ const { scope, resource } = ctx.body;
770
+ const resources = toResourceList(resource);
699
771
  if (!client_id) throw new APIError("BAD_REQUEST", {
700
772
  error_description: "Missing required client_id",
701
773
  error: "invalid_grant"
@@ -726,7 +798,8 @@ async function handleClientCredentialsGrant(ctx, opts) {
726
798
  return createUserTokens(ctx, opts, {
727
799
  client,
728
800
  scopes: requestedScopes,
729
- grantType: "client_credentials"
801
+ grantType: "client_credentials",
802
+ resources
730
803
  });
731
804
  }
732
805
  /**
@@ -737,7 +810,8 @@ async function handleClientCredentialsGrant(ctx, opts) {
737
810
  */
738
811
  async function handleRefreshTokenGrant(ctx, opts) {
739
812
  const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
740
- const { refresh_token, scope } = ctx.body;
813
+ const { refresh_token, scope, resource } = ctx.body;
814
+ const resources = toResourceList(resource);
741
815
  if (!client_id) throw new APIError("BAD_REQUEST", {
742
816
  error_description: "Missing required client_id",
743
817
  error: "invalid_grant"
@@ -767,21 +841,16 @@ async function handleRefreshTokenGrant(ctx, opts) {
767
841
  error: "invalid_grant"
768
842
  });
769
843
  if (refreshToken.revoked) {
770
- await ctx.context.adapter.deleteMany({
771
- model: "oauthRefreshToken",
772
- where: [{
773
- field: "clientId",
774
- value: client_id
775
- }, {
776
- field: "userId",
777
- value: refreshToken.userId
778
- }]
779
- });
844
+ await invalidateRefreshFamily(ctx, client_id, refreshToken.userId);
780
845
  throw new APIError("BAD_REQUEST", {
781
846
  error_description: "invalid refresh token",
782
847
  error: "invalid_grant"
783
848
  });
784
849
  }
850
+ if (resources && refreshToken.resources && !resources.every((v) => refreshToken.resources?.includes(v))) throw new APIError("BAD_REQUEST", {
851
+ error_description: "requested resource invalid",
852
+ error: "invalid_target"
853
+ });
785
854
  const scopes = refreshToken?.scopes;
786
855
  const requestedScopes = scope?.split(" ");
787
856
  if (requestedScopes) {
@@ -806,6 +875,7 @@ async function handleRefreshTokenGrant(ctx, opts) {
806
875
  referenceId: refreshToken.referenceId,
807
876
  sessionId: refreshToken.sessionId,
808
877
  refreshToken,
878
+ resources: resources ?? refreshToken.resources,
809
879
  authTime
810
880
  });
811
881
  }
@@ -913,10 +983,17 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
913
983
  }
914
984
  let user;
915
985
  if (accessToken.userId) user = await ctx.context.internalAdapter.findUserById(accessToken?.userId);
986
+ const resources = Array.isArray(accessToken.resources) ? accessToken.resources : void 0;
987
+ const audience = resources ? [...resources] : void 0;
988
+ if (audience?.length && accessToken.scopes?.includes("openid")) {
989
+ const userInfoEndpoint = `${ctx.context.baseURL}/oauth2/userinfo`;
990
+ if (!audience.includes(userInfoEndpoint)) audience.push(userInfoEndpoint);
991
+ }
916
992
  const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
917
993
  user,
918
994
  scopes: accessToken.scopes,
919
995
  referenceId: accessToken?.referenceId,
996
+ resources,
920
997
  metadata: parseClientMetadata(client?.metadata)
921
998
  }) : {};
922
999
  const jwtPluginOptions = (opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options;
@@ -924,6 +1001,7 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
924
1001
  ...customClaims,
925
1002
  active: true,
926
1003
  iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
1004
+ aud: toAudienceClaim(audience),
927
1005
  client_id: accessToken.clientId,
928
1006
  sub: user?.id,
929
1007
  sid: sessionId,
@@ -1287,6 +1365,28 @@ const publicSessionMiddleware = (opts) => createAuthMiddleware(async (ctx) => {
1287
1365
  if (!await verifyOAuthQueryParams(query, ctx.context.secret)) throw new APIError("UNAUTHORIZED", { error: "invalid_signature" });
1288
1366
  });
1289
1367
  //#endregion
1368
+ //#region src/oauthClient/privileges.ts
1369
+ /**
1370
+ * Authorizes a client action against the configured `clientPrivileges` hook.
1371
+ *
1372
+ * This is the single authorization helper for every OAuth client mutation. The
1373
+ * create path enforces it at the shared creation chokepoint so that no
1374
+ * registration route can reach client persistence without it.
1375
+ *
1376
+ * @throws APIError UNAUTHORIZED when there is no session or the hook denies the action.
1377
+ * @throws APIError BAD_REQUEST when the request carries no headers.
1378
+ */
1379
+ async function assertClientPrivileges(ctx, session, opts, action) {
1380
+ if (!session) throw new APIError("UNAUTHORIZED");
1381
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1382
+ if (opts.clientPrivileges && !await opts.clientPrivileges({
1383
+ headers: ctx.headers,
1384
+ action,
1385
+ session: session.session,
1386
+ user: session.user
1387
+ })) throw new APIError("UNAUTHORIZED");
1388
+ }
1389
+ //#endregion
1290
1390
  //#region src/register.ts
1291
1391
  /**
1292
1392
  * Resolves the auth method and type for unauthenticated DCR.
@@ -1422,6 +1522,9 @@ async function checkOAuthClient(client, opts, settings) {
1422
1522
  async function createOAuthClientEndpoint(ctx, opts, settings) {
1423
1523
  const body = ctx.body;
1424
1524
  const session = await getSessionFromCtx(ctx);
1525
+ if (settings.isRegister) {
1526
+ if (session) await assertClientPrivileges(ctx, session, opts, "create");
1527
+ } else await assertClientPrivileges(ctx, session, opts, "create");
1425
1528
  const isPublic = body.token_endpoint_auth_method === "none";
1426
1529
  const isPrivateKeyJwt = body.token_endpoint_auth_method === "private_key_jwt";
1427
1530
  await checkOAuthClient(ctx.body, opts, {
@@ -1767,16 +1870,6 @@ async function rotateClientSecretEndpoint(ctx, opts) {
1767
1870
  clientSecret: (opts.prefix?.clientSecret ?? "") + clientSecret
1768
1871
  });
1769
1872
  }
1770
- async function assertClientPrivileges(ctx, session, opts, action) {
1771
- if (!session) throw new APIError("UNAUTHORIZED");
1772
- if (!ctx.headers) throw new APIError("BAD_REQUEST");
1773
- if (opts.clientPrivileges && !await opts.clientPrivileges({
1774
- headers: ctx.headers,
1775
- action,
1776
- session: session.session,
1777
- user: session.user
1778
- })) throw new APIError("UNAUTHORIZED");
1779
- }
1780
1873
  //#endregion
1781
1874
  //#region src/oauthClient/index.ts
1782
1875
  const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/create-client", {
@@ -1800,7 +1893,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
1800
1893
  "client_secret_post",
1801
1894
  "private_key_jwt"
1802
1895
  ]).default("client_secret_basic").optional(),
1803
- jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
1896
+ 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(),
1804
1897
  jwks_uri: z.string().optional(),
1805
1898
  grant_types: z.array(z.enum([
1806
1899
  "authorization_code",
@@ -1962,7 +2055,6 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
1962
2055
  }
1963
2056
  }
1964
2057
  }, async (ctx) => {
1965
- await assertClientPrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
1966
2058
  return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
1967
2059
  });
1968
2060
  const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client", {
@@ -1987,7 +2079,7 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
1987
2079
  "client_secret_post",
1988
2080
  "private_key_jwt"
1989
2081
  ]).default("client_secret_basic").optional(),
1990
- jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
2082
+ 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(),
1991
2083
  jwks_uri: z.string().optional(),
1992
2084
  grant_types: z.array(z.enum([
1993
2085
  "authorization_code",
@@ -2135,7 +2227,6 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
2135
2227
  } }
2136
2228
  } }
2137
2229
  }, async (ctx) => {
2138
- await assertClientPrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
2139
2230
  return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
2140
2231
  });
2141
2232
  const getOAuthClient = (opts) => createAuthEndpoint("/oauth2/get-client", {
@@ -2339,12 +2430,12 @@ async function updateConsentEndpoint(ctx, opts) {
2339
2430
  error_description: "no consent",
2340
2431
  error: "not_found"
2341
2432
  });
2433
+ if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
2342
2434
  const client = await getClient(ctx, opts, consent.clientId);
2343
- if (!consent) throw new APIError("NOT_FOUND", {
2344
- error_description: "no consent",
2435
+ if (!client) throw new APIError("NOT_FOUND", {
2436
+ error_description: "client not found",
2345
2437
  error: "not_found"
2346
2438
  });
2347
- if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
2348
2439
  const allowedScopes = client?.scopes ?? opts.scopes ?? [];
2349
2440
  const updates = ctx.body.update;
2350
2441
  const scopes = updates.scopes;
@@ -2492,16 +2583,7 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
2492
2583
  error: "invalid_request"
2493
2584
  });
2494
2585
  if (refreshToken.revoked) {
2495
- await ctx.context.adapter.deleteMany({
2496
- model: "oauthRefreshToken",
2497
- where: [{
2498
- field: "clientId",
2499
- value: clientId
2500
- }, {
2501
- field: "userId",
2502
- value: refreshToken.userId
2503
- }]
2504
- });
2586
+ await invalidateRefreshFamily(ctx, clientId, refreshToken.userId);
2505
2587
  throw new APIError$1("BAD_REQUEST", {
2506
2588
  error_description: "refresh token revoked",
2507
2589
  error: "invalid_request"
@@ -2509,20 +2591,31 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
2509
2591
  }
2510
2592
  if (!refreshToken.clientId || refreshToken.clientId !== clientId) return null;
2511
2593
  const iat = Math.floor(Date.now() / 1e3);
2512
- await Promise.allSettled([ctx.context.adapter.deleteMany({
2513
- model: "oauthAccessToken",
2514
- where: [{
2515
- field: "refreshId",
2516
- value: refreshToken.id
2517
- }]
2518
- }), ctx.context.adapter.update({
2594
+ if (!await ctx.context.adapter.update({
2519
2595
  model: "oauthRefreshToken",
2520
2596
  where: [{
2521
2597
  field: "id",
2522
2598
  value: refreshToken.id
2599
+ }, {
2600
+ field: "revoked",
2601
+ operator: "eq",
2602
+ value: null
2523
2603
  }],
2524
2604
  update: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
2525
- })]);
2605
+ })) {
2606
+ await invalidateRefreshFamily(ctx, clientId, refreshToken.userId);
2607
+ throw new APIError$1("BAD_REQUEST", {
2608
+ error_description: "refresh token revoked",
2609
+ error: "invalid_request"
2610
+ });
2611
+ }
2612
+ await ctx.context.adapter.deleteMany({
2613
+ model: "oauthAccessToken",
2614
+ where: [{
2615
+ field: "refreshId",
2616
+ value: refreshToken.id
2617
+ }]
2618
+ });
2526
2619
  }
2527
2620
  /**
2528
2621
  * We don't know the access token format so we try to validate it
@@ -2736,7 +2829,8 @@ const schema = {
2736
2829
  oauthRefreshToken: { fields: {
2737
2830
  token: {
2738
2831
  type: "string",
2739
- required: true
2832
+ required: true,
2833
+ unique: true
2740
2834
  },
2741
2835
  clientId: {
2742
2836
  type: "string",
@@ -2770,6 +2864,10 @@ const schema = {
2770
2864
  type: "string",
2771
2865
  required: false
2772
2866
  },
2867
+ resources: {
2868
+ type: "string[]",
2869
+ required: false
2870
+ },
2773
2871
  expiresAt: { type: "date" },
2774
2872
  createdAt: { type: "date" },
2775
2873
  revoked: {
@@ -2824,6 +2922,10 @@ const schema = {
2824
2922
  type: "string",
2825
2923
  required: false
2826
2924
  },
2925
+ resources: {
2926
+ type: "string[]",
2927
+ required: false
2928
+ },
2827
2929
  refreshId: {
2828
2930
  type: "string",
2829
2931
  required: false,
@@ -2866,6 +2968,10 @@ const schema = {
2866
2968
  type: "string",
2867
2969
  required: false
2868
2970
  },
2971
+ resources: {
2972
+ type: "string[]",
2973
+ required: false
2974
+ },
2869
2975
  scopes: {
2870
2976
  type: "string[]",
2871
2977
  required: true
@@ -2943,10 +3049,55 @@ const oauthProvider = (options) => {
2943
3049
  if (opts.grantTypes && opts.grantTypes.includes("refresh_token") && !opts.grantTypes.includes("authorization_code")) throw new BetterAuthError("refresh_token grant requires authorization_code grant");
2944
3050
  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");
2945
3051
  if (!opts.disableJwtPlugin && (opts.storeClientSecret === "encrypted" || typeof opts.storeClientSecret === "object" && ("encrypt" in opts.storeClientSecret || "decrypt" in opts.storeClientSecret))) throw new BetterAuthError("encryption method not recommended, please use 'hashed' or the 'hash' function");
3052
+ const handleIssuerMetadataRequest = async (request, ctx) => {
3053
+ const requestPathname = new URL(request.url).pathname;
3054
+ const requestPath = ctx.options.advanced?.skipTrailingSlashes ? requestPathname.replace(/\/+$/, "") || "/" : requestPathname;
3055
+ const issuer = opts.disableJwtPlugin ? ctx.baseURL : getJwtPlugin(ctx)?.options?.jwt?.issuer ?? ctx.baseURL;
3056
+ let issuerPath = "/";
3057
+ try {
3058
+ issuerPath = new URL(issuer).pathname.replace(/\/$/, "") || "";
3059
+ } catch {
3060
+ issuerPath = new URL(ctx.baseURL).pathname.replace(/\/$/, "") || "";
3061
+ }
3062
+ const endpointCtx = { context: ctx };
3063
+ const authServerMetadataPaths = new Set([`/.well-known/oauth-authorization-server${issuerPath}`, `${issuerPath}/.well-known/oauth-authorization-server`]);
3064
+ const openIdConfigPath = `${issuerPath}/.well-known/openid-configuration`;
3065
+ const isAuthServerMetadataRequest = authServerMetadataPaths.has(requestPath);
3066
+ const isOpenIdConfigRequest = opts.scopes?.includes("openid") && requestPath === openIdConfigPath;
3067
+ const createMetadataResponse = (metadata) => {
3068
+ const response = metadataResponse(metadata);
3069
+ if (request.method === "HEAD") return new Response(null, {
3070
+ status: response.status,
3071
+ headers: response.headers
3072
+ });
3073
+ return response;
3074
+ };
3075
+ if (isAuthServerMetadataRequest || isOpenIdConfigRequest) {
3076
+ if (request.method !== "GET" && request.method !== "HEAD") return { response: new Response(null, {
3077
+ status: 405,
3078
+ headers: { Allow: "GET, HEAD" }
3079
+ }) };
3080
+ }
3081
+ if (isAuthServerMetadataRequest) {
3082
+ if (opts.scopes?.includes("openid")) return { response: createMetadataResponse(oidcServerMetadata(endpointCtx, opts)) };
3083
+ return { response: createMetadataResponse({
3084
+ ...authServerMetadata(endpointCtx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx)?.options, {
3085
+ scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
3086
+ dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
3087
+ public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
3088
+ grant_types_supported: opts.grantTypes,
3089
+ jwt_disabled: opts.disableJwtPlugin
3090
+ }),
3091
+ ...mergeDiscoveryMetadata(opts.clientDiscovery)
3092
+ }) };
3093
+ }
3094
+ if (isOpenIdConfigRequest) return { response: createMetadataResponse(oidcServerMetadata(endpointCtx, opts)) };
3095
+ };
2946
3096
  return {
2947
3097
  id: "oauth-provider",
2948
3098
  version: PACKAGE_VERSION,
2949
3099
  options: opts,
3100
+ onRequest: handleIssuerMetadataRequest,
2950
3101
  init: (ctx) => {
2951
3102
  if (ctx.options.secondaryStorage && ctx.options.session?.storeSessionInDatabase !== true) throw new BetterAuthError("OAuth Provider requires `session.storeSessionInDatabase: true` when using secondaryStorage");
2952
3103
  if (!opts.disableJwtPlugin) {
@@ -3021,6 +3172,7 @@ const oauthProvider = (options) => {
3021
3172
  else return {
3022
3173
  ...authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
3023
3174
  scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
3175
+ dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
3024
3176
  public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
3025
3177
  grant_types_supported: opts.grantTypes,
3026
3178
  jwt_disabled: opts.disableJwtPlugin
@@ -3047,6 +3199,7 @@ const oauthProvider = (options) => {
3047
3199
  code_challenge: z.string().optional(),
3048
3200
  code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
3049
3201
  nonce: z.string().optional(),
3202
+ resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional(),
3050
3203
  prompt: z.string().pipe(z.enum([
3051
3204
  "none",
3052
3205
  "consent",
@@ -3058,7 +3211,10 @@ const oauthProvider = (options) => {
3058
3211
  ])).optional()
3059
3212
  }),
3060
3213
  redirectOnError: authorizeRedirectOnError(opts),
3061
- errorCodesByField: { response_type: { invalid: "unsupported_response_type" } },
3214
+ errorCodesByField: {
3215
+ response_type: { invalid: "unsupported_response_type" },
3216
+ resource: { invalid: "invalid_target" }
3217
+ },
3062
3218
  metadata: { openapi: {
3063
3219
  description: "Authorize an OAuth2 request",
3064
3220
  parameters: [
@@ -3128,6 +3284,16 @@ const oauthProvider = (options) => {
3128
3284
  schema: { type: "string" },
3129
3285
  description: "OpenID Connect nonce"
3130
3286
  },
3287
+ {
3288
+ name: "resource",
3289
+ in: "query",
3290
+ required: false,
3291
+ schema: {
3292
+ type: "array",
3293
+ items: { type: "string" }
3294
+ },
3295
+ 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."
3296
+ },
3131
3297
  {
3132
3298
  name: "prompt",
3133
3299
  in: "query",
@@ -3233,13 +3399,16 @@ const oauthProvider = (options) => {
3233
3399
  code_verifier: z.string().optional(),
3234
3400
  redirect_uri: SafeUrlSchema.optional(),
3235
3401
  refresh_token: z.string().optional(),
3236
- resource: z.string().optional(),
3402
+ resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional(),
3237
3403
  scope: z.string().optional()
3238
3404
  }),
3239
- errorCodesByField: { grant_type: {
3240
- missing: "invalid_request",
3241
- invalid: "unsupported_grant_type"
3242
- } },
3405
+ errorCodesByField: {
3406
+ grant_type: {
3407
+ missing: "invalid_request",
3408
+ invalid: "unsupported_grant_type"
3409
+ },
3410
+ resource: { invalid: "invalid_target" }
3411
+ },
3243
3412
  metadata: {
3244
3413
  allowedMediaTypes: ["application/x-www-form-urlencoded"],
3245
3414
  openapi: {
@@ -3284,8 +3453,15 @@ const oauthProvider = (options) => {
3284
3453
  description: "Refresh token (for refresh_token grant)"
3285
3454
  },
3286
3455
  resource: {
3287
- type: "string",
3288
- description: "Requested token resource (ie audience) to obtain a JWT formatted access token"
3456
+ oneOf: [{
3457
+ type: "string",
3458
+ description: "Single resource (URL)"
3459
+ }, {
3460
+ type: "array",
3461
+ items: { type: "string" },
3462
+ description: "Multiple resources (URLs)"
3463
+ }],
3464
+ description: "Requested token resource(s) (ie audience) to obtain a JWT formatted access token"
3289
3465
  },
3290
3466
  scope: {
3291
3467
  type: "string",
@@ -3386,10 +3562,6 @@ const oauthProvider = (options) => {
3386
3562
  token_type_hint: {
3387
3563
  type: "string",
3388
3564
  description: "Hint about the token type. Recognized values: `access_token`, `refresh_token`."
3389
- },
3390
- resource: {
3391
- type: "string",
3392
- description: "Introspects a token for a specific resource."
3393
3565
  }
3394
3566
  },
3395
3567
  required: ["token"]
@@ -3677,7 +3849,7 @@ const oauthProvider = (options) => {
3677
3849
  "client_secret_post",
3678
3850
  "private_key_jwt"
3679
3851
  ]).default("client_secret_basic").optional(),
3680
- jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
3852
+ 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(),
3681
3853
  jwks_uri: z.string().optional(),
3682
3854
  grant_types: z.array(z.enum([
3683
3855
  "authorization_code",
@@ -4082,6 +4254,9 @@ async function authorizeEndpoint(ctx, opts, settings) {
4082
4254
  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)));
4083
4255
  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)));
4084
4256
  }
4257
+ const resource = query.resource;
4258
+ 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)));
4259
+ const requestedResources = toResourceList(resource) ?? [];
4085
4260
  const session = await getSessionFromCtx(ctx);
4086
4261
  if (!session || promptSet?.has("login") || promptSet?.has("create")) {
4087
4262
  if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "login_required", "authentication required");
@@ -4134,7 +4309,8 @@ async function authorizeEndpoint(ctx, opts, settings) {
4134
4309
  userId: session.user.id,
4135
4310
  sessionId: session.session.id,
4136
4311
  authTime: new Date(session.session.createdAt).getTime(),
4137
- referenceId
4312
+ referenceId,
4313
+ resource: requestedResources
4138
4314
  });
4139
4315
  const consent = await ctx.context.adapter.findOne({
4140
4316
  model: "oauthConsent",
@@ -4157,13 +4333,19 @@ async function authorizeEndpoint(ctx, opts, settings) {
4157
4333
  if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "consent_required", "End-User consent is required");
4158
4334
  return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
4159
4335
  }
4336
+ const consentedResources = consent?.resources ?? [];
4337
+ if (requestedResources.some((requestedResource) => !consentedResources.includes(requestedResource))) {
4338
+ if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "consent_required", "End-User consent is required");
4339
+ return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
4340
+ }
4160
4341
  return redirectWithAuthorizationCode(ctx, opts, {
4161
4342
  query,
4162
4343
  clientId: client.clientId,
4163
4344
  userId: session.user.id,
4164
4345
  sessionId: session.session.id,
4165
4346
  authTime: new Date(session.session.createdAt).getTime(),
4166
- referenceId
4347
+ referenceId,
4348
+ resource: requestedResources
4167
4349
  });
4168
4350
  }
4169
4351
  function serializeAuthorizationQuery(query) {
@@ -4189,7 +4371,8 @@ async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
4189
4371
  userId: verificationValue.userId,
4190
4372
  sessionId: verificationValue?.sessionId,
4191
4373
  referenceId: verificationValue.referenceId,
4192
- authTime: verificationValue.authTime
4374
+ authTime: verificationValue.authTime,
4375
+ resource: verificationValue.resource
4193
4376
  })
4194
4377
  };
4195
4378
  await ctx.context.internalAdapter.createVerificationValue({
@@ -4235,7 +4418,7 @@ function authServerMetadata(ctx, opts, overrides) {
4235
4418
  authorization_endpoint: `${baseURL}/oauth2/authorize`,
4236
4419
  token_endpoint: `${baseURL}/oauth2/token`,
4237
4420
  jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
4238
- registration_endpoint: `${baseURL}/oauth2/register`,
4421
+ registration_endpoint: overrides?.dynamic_client_registration_supported ? `${baseURL}/oauth2/register` : void 0,
4239
4422
  introspection_endpoint: `${baseURL}/oauth2/introspect`,
4240
4423
  revocation_endpoint: `${baseURL}/oauth2/revoke`,
4241
4424
  response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
@@ -4251,19 +4434,19 @@ function authServerMetadata(ctx, opts, overrides) {
4251
4434
  "client_secret_post",
4252
4435
  "private_key_jwt"
4253
4436
  ],
4254
- token_endpoint_auth_signing_alg_values_supported: [...ASSERTION_SIGNING_ALGORITHMS],
4437
+ token_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
4255
4438
  introspection_endpoint_auth_methods_supported: [
4256
4439
  "client_secret_basic",
4257
4440
  "client_secret_post",
4258
4441
  "private_key_jwt"
4259
4442
  ],
4260
- introspection_endpoint_auth_signing_alg_values_supported: [...ASSERTION_SIGNING_ALGORITHMS],
4443
+ introspection_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
4261
4444
  revocation_endpoint_auth_methods_supported: [
4262
4445
  "client_secret_basic",
4263
4446
  "client_secret_post",
4264
4447
  "private_key_jwt"
4265
4448
  ],
4266
- revocation_endpoint_auth_signing_alg_values_supported: [...ASSERTION_SIGNING_ALGORITHMS],
4449
+ revocation_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
4267
4450
  code_challenge_methods_supported: ["S256"],
4268
4451
  authorization_response_iss_parameter_supported: true
4269
4452
  };
@@ -4274,6 +4457,7 @@ function oidcServerMetadata(ctx, opts) {
4274
4457
  return {
4275
4458
  ...authServerMetadata(ctx, jwtPluginOptions, {
4276
4459
  scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
4460
+ dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
4277
4461
  public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
4278
4462
  grant_types_supported: opts.grantTypes,
4279
4463
  jwt_disabled: opts.disableJwtPlugin