@better-auth/oauth-provider 1.7.0-beta.8 → 1.7.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
- import { A as collectExtensionUserInfoClaims, C as toAudienceClaim, F as getSupportedGrantTypes, I as hasUserInfoClaimExtension, N as getExtensionGrantHandler, O as collectExtensionAccessTokenClaims, S as storeToken, T as validateClientCredentials, a as getClient, c as getStoredToken, f as normalizeTimestampValue, i as extractClientCredentials, k as collectExtensionIdTokenClaims, l as isPKCERequired, m as parseClientMetadata, n as decryptStoredClientSecret, o as getJwtPlugin, r as destructureCredentials, t as clientAllowsGrant, v as resolveSessionAuthTime, w as toResourceList, y as resolveSubjectIdentifier } from "./utils-Baq6atYN.mjs";
2
+ import { A as collectExtensionUserInfoClaims, C as toAudienceClaim, F as getSupportedGrantTypes, I as hasUserInfoClaimExtension, N as getExtensionGrantHandler, O as collectExtensionAccessTokenClaims, S as storeToken, T as validateClientCredentials, a as getClient, c as getStoredToken, f as normalizeTimestampValue, i as extractClientCredentials, k as collectExtensionIdTokenClaims, l as isPKCERequired, m as parseClientMetadata, n as decryptStoredClientSecret, o as getJwtPlugin, r as destructureCredentials, t as clientAllowsGrant, v as resolveSessionAuthTime, w as toResourceList, y as resolveSubjectIdentifier } from "./utils-DO8lmoDw.mjs";
3
3
  import { APIError } from "better-auth/api";
4
- import { generateRandomString } from "better-auth/crypto";
4
+ import { generateRandomString, symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
5
5
  import { APIError as APIError$1 } from "better-call";
6
6
  import { logger } from "@better-auth/core/env";
7
7
  import * as z from "zod";
@@ -9,6 +9,156 @@ import { createDpopReplayStore, enforceDpopBinding, generateCodeChallenge, getCo
9
9
  import { SignJWT, base64url, createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
10
10
  import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
11
11
  import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
12
+ //#region src/authentication-context.ts
13
+ const RESERVED_ID_TOKEN_CLAIMS = new Set([
14
+ "iss",
15
+ "sub",
16
+ "aud",
17
+ "exp",
18
+ "nbf",
19
+ "iat",
20
+ "jti",
21
+ "auth_time",
22
+ "nonce",
23
+ "acr",
24
+ "amr",
25
+ "azp",
26
+ "sid",
27
+ "at_hash",
28
+ "c_hash",
29
+ "s_hash"
30
+ ]);
31
+ /**
32
+ * Removes provider-owned ID token claim names from custom claims.
33
+ *
34
+ * `customIdTokenClaims` is an extension point for additional claims. It must not
35
+ * replace the issuer, subject, audience, lifetime, nonce, authorized party,
36
+ * token-binding hashes, session binding, or authentication context that the
37
+ * provider owns.
38
+ */
39
+ function stripReservedIdTokenClaims(claims) {
40
+ if (!claims) return {};
41
+ const stripped = [];
42
+ const safeClaims = Object.create(null);
43
+ for (const [key, value] of Object.entries(claims)) {
44
+ if (RESERVED_ID_TOKEN_CLAIMS.has(key)) {
45
+ stripped.push(key);
46
+ continue;
47
+ }
48
+ safeClaims[key] = value;
49
+ }
50
+ if (stripped.length > 0) logger.warn(`oauth-provider: stripped reserved id-token claim name(s): ${stripped.join(", ")}. The AS owns these claim values.`);
51
+ return safeClaims;
52
+ }
53
+ //#endregion
54
+ //#region src/claims-request.ts
55
+ const claimRequestMemberSchema = z.union([z.null(), z.record(z.string(), z.unknown())]);
56
+ const claimsRequestObjectSchema = z.object({
57
+ userinfo: z.record(z.string(), claimRequestMemberSchema).optional(),
58
+ id_token: z.record(z.string(), claimRequestMemberSchema).optional()
59
+ }).passthrough();
60
+ function parseClaimsRequestValue(value) {
61
+ if (typeof value === "string") try {
62
+ return JSON.parse(value);
63
+ } catch {
64
+ return;
65
+ }
66
+ return value;
67
+ }
68
+ function parseClaimsRequestObject(value) {
69
+ const parsed = parseClaimsRequestValue(value);
70
+ const result = claimsRequestObjectSchema.safeParse(parsed);
71
+ return result.success ? result.data : void 0;
72
+ }
73
+ const claimsRequestParameterSchema = z.union([z.string(), z.record(z.string(), z.unknown())]).superRefine((value, ctx) => {
74
+ if (!parseClaimsRequestObject(value)) ctx.addIssue({
75
+ code: "custom",
76
+ message: "claims must be a JSON object"
77
+ });
78
+ });
79
+ function getRequestedUserInfoClaims(value, supportedClaims) {
80
+ const userInfoClaims = parseClaimsRequestObject(value)?.userinfo;
81
+ if (!userInfoClaims) return [];
82
+ const names = Object.keys(userInfoClaims);
83
+ if (!supportedClaims) return names;
84
+ const allowed = new Set(supportedClaims);
85
+ return names.filter((name) => allowed.has(name));
86
+ }
87
+ function filterClaimsRequestUserInfoClaims(value, allowedUserInfoClaims) {
88
+ const claimsRequest = parseClaimsRequestObject(value);
89
+ if (!claimsRequest) return void 0;
90
+ const allowedClaimSet = new Set(allowedUserInfoClaims);
91
+ const userInfoClaims = Object.fromEntries(Object.entries(claimsRequest.userinfo ?? {}).filter(([claim]) => allowedClaimSet.has(claim)));
92
+ const filteredClaimsRequest = Object.keys(userInfoClaims).length ? {
93
+ ...claimsRequest,
94
+ userinfo: userInfoClaims
95
+ } : Object.fromEntries(Object.entries(claimsRequest).filter(([key]) => key !== "userinfo"));
96
+ return Object.keys(filteredClaimsRequest).length ? filteredClaimsRequest : void 0;
97
+ }
98
+ //#endregion
99
+ //#region src/standard-claims.ts
100
+ function splitDisplayName(name) {
101
+ const parts = name.split(" ").filter((part) => part !== "");
102
+ if (parts.length <= 1) return {};
103
+ return {
104
+ given: parts.slice(0, -1).join(" "),
105
+ family: parts.at(-1)
106
+ };
107
+ }
108
+ /**
109
+ * The OIDC Standard Claims (OIDC Core §5.1) that Better Auth resolves from its
110
+ * own user model, each paired with the scope that requests it (§5.4).
111
+ *
112
+ * This is the single source for UserInfo scope-claim resolution, individual
113
+ * `claims.userinfo` resolution, the discovery `claims_supported` advertisement,
114
+ * and the bound on requested claim names. Adding a standard claim Better Auth
115
+ * can resolve means adding one entry here, not editing UserInfo, the metadata,
116
+ * and the plugin init separately.
117
+ *
118
+ * These claims are delivered only at the UserInfo endpoint, never the ID token:
119
+ * the authorization code flow always issues an access token, so §5.4 routes
120
+ * scope-requested claims to UserInfo. Deployments that support further standard
121
+ * profile claims (such as `birthdate` or `zoneinfo`) supply them through
122
+ * `customUserInfoClaims` and advertise them in `claims_supported`.
123
+ */
124
+ const STANDARD_CLAIMS = {
125
+ name: {
126
+ scope: "profile",
127
+ resolve: (user) => user.name ?? void 0
128
+ },
129
+ picture: {
130
+ scope: "profile",
131
+ resolve: (user) => user.image ?? void 0
132
+ },
133
+ given_name: {
134
+ scope: "profile",
135
+ resolve: (user) => splitDisplayName(user.name).given
136
+ },
137
+ family_name: {
138
+ scope: "profile",
139
+ resolve: (user) => splitDisplayName(user.name).family
140
+ },
141
+ email: {
142
+ scope: "email",
143
+ resolve: (user) => user.email ?? void 0
144
+ },
145
+ email_verified: {
146
+ scope: "email",
147
+ resolve: (user) => user.emailVerified ?? false
148
+ }
149
+ };
150
+ const STANDARD_CLAIM_NAMES = Object.keys(STANDARD_CLAIMS);
151
+ /**
152
+ * The claim names this provider advertises it can supply (discovery
153
+ * `claims_supported`): the operator's `advertisedMetadata.claims_supported`
154
+ * override when set, otherwise the scope-derived standard set computed at plugin
155
+ * init. Used both to advertise and to bound requested `claims.userinfo` names so
156
+ * a client cannot pin arbitrary attacker-controlled strings onto stored tokens.
157
+ */
158
+ function getSupportedClaims(opts) {
159
+ return opts.advertisedMetadata?.claims_supported ?? opts.claims ?? [];
160
+ }
161
+ //#endregion
12
162
  //#region src/claims.ts
13
163
  /**
14
164
  * Claim names the authorization server owns on a JWT access token, which no
@@ -851,11 +1001,12 @@ const dpopJktSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/, "dpop_jkt must be
851
1001
  */
852
1002
  const authorizationQuerySchema = z.object({
853
1003
  response_type: z.string().pipe(z.enum(["code"])).optional(),
1004
+ request: z.string().optional(),
854
1005
  request_uri: z.string().optional(),
855
1006
  redirect_uri: SafeUrlSchema.optional(),
856
1007
  scope: z.string().optional(),
857
1008
  state: z.string().optional(),
858
- client_id: z.string(),
1009
+ client_id: z.string().min(1, "client_id is required"),
859
1010
  prompt: authorizationPromptSchema.optional(),
860
1011
  display: z.string().optional(),
861
1012
  ui_locales: z.string().optional(),
@@ -866,10 +1017,11 @@ const authorizationQuerySchema = z.object({
866
1017
  code_challenge: z.string().optional(),
867
1018
  code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
868
1019
  nonce: z.string().optional(),
1020
+ claims: claimsRequestParameterSchema.optional(),
869
1021
  dpop_jkt: dpopJktSchema.optional(),
870
1022
  resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional()
871
1023
  }).passthrough();
872
- const storedAuthorizationQuerySchema = authorizationQuerySchema.extend({ redirect_uri: SafeUrlSchema });
1024
+ const storedAuthorizationQuerySchema = authorizationQuerySchema.extend({ redirect_uri: SafeUrlSchema.optional() });
873
1025
  /**
874
1026
  * Runtime schema for the authorization code verification value.
875
1027
  * Validates structure on deserialization from the JSON blob stored in the DB.
@@ -930,27 +1082,20 @@ const clientRegistrationRequestSchema = z.object({
930
1082
  //#endregion
931
1083
  //#region src/userinfo.ts
932
1084
  /**
933
- * Provides shared /userinfo and id_token claims functionality
1085
+ * Builds the standard OIDC claims (OIDC Core §5.1) for the UserInfo response.
1086
+ *
1087
+ * A claim is included when its backing scope was granted, or when it was named
1088
+ * individually through the `claims.userinfo` request parameter (§5.4, §5.5).
1089
+ * `sub` is always present. Values come from the one claim registry, so the
1090
+ * advertisement, the scope mapping, and the resolution cannot drift apart.
934
1091
  *
935
1092
  * @see https://openid.net/specs/openid-connect-core-1_0.html#NormalClaims
936
1093
  */
937
- function userNormalClaims(user, scopes) {
938
- const name = user.name.split(" ").filter((v) => v !== "");
939
- const profile = {
940
- name: user.name ?? void 0,
941
- picture: user.image ?? void 0,
942
- given_name: name.length > 1 ? name.slice(0, -1).join(" ") : void 0,
943
- family_name: name.length > 1 ? name.at(-1) : void 0
944
- };
945
- const email = {
946
- email: user.email ?? void 0,
947
- email_verified: user.emailVerified ?? false
948
- };
949
- return {
950
- sub: user.id ?? void 0,
951
- ...scopes.includes("profile") ? profile : {},
952
- ...scopes.includes("email") ? email : {}
953
- };
1094
+ function userNormalClaims(user, scopes, requestedClaims = []) {
1095
+ const requested = new Set(requestedClaims);
1096
+ const claims = { sub: user.id ?? void 0 };
1097
+ for (const [name, definition] of Object.entries(STANDARD_CLAIMS)) if (scopes.includes(definition.scope) || requested.has(name)) claims[name] = definition.resolve(user);
1098
+ return claims;
954
1099
  }
955
1100
  /**
956
1101
  * Returns the defined-valued entries of `claims`, dropping any key already
@@ -976,17 +1121,33 @@ function pickClaims(claims, base) {
976
1121
  }
977
1122
  return next;
978
1123
  }
1124
+ function getUserInfoAccessToken(ctx) {
1125
+ const authorization = ctx.headers?.get("authorization");
1126
+ const headerAccessTokenAuthorization = parseAccessTokenAuthorization(authorization);
1127
+ const bodyToken = ctx.request?.method === "POST" ? ctx.body?.access_token ?? void 0 : void 0;
1128
+ if (headerAccessTokenAuthorization && bodyToken) throw new APIError("BAD_REQUEST", {
1129
+ error_description: "Multiple access token transport methods are not allowed",
1130
+ error: "invalid_request"
1131
+ });
1132
+ const accessTokenAuthorization = headerAccessTokenAuthorization ?? (bodyToken ? {
1133
+ scheme: "Bearer",
1134
+ token: bodyToken
1135
+ } : void 0);
1136
+ return {
1137
+ authorization: accessTokenAuthorization,
1138
+ token: accessTokenAuthorization?.token
1139
+ };
1140
+ }
979
1141
  /**
980
1142
  * Handles the /oauth2/userinfo endpoint
981
1143
  */
982
1144
  async function userInfoEndpoint(ctx, opts) {
983
- const authorization = ctx.headers?.get("authorization");
984
- const accessTokenAuthorization = parseAccessTokenAuthorization(authorization);
1145
+ const { authorization: accessTokenAuthorization } = getUserInfoAccessToken(ctx);
985
1146
  if (!accessTokenAuthorization?.token) throw new APIError("UNAUTHORIZED", {
986
- error_description: "authorization header not found",
1147
+ error_description: "access token not found",
987
1148
  error: "invalid_request"
988
1149
  });
989
- const jwt = await requireActiveAccessToken(ctx, opts, accessTokenAuthorization.token);
1150
+ const { payload: jwt, requestedUserInfoClaims: requestedClaims } = await requireActiveAccessTokenWithClaims(ctx, opts, accessTokenAuthorization.token);
990
1151
  if (getDpopJktFromPayload(jwt) && !ctx.request) throw new APIError("UNAUTHORIZED", {
991
1152
  error_description: "DPoP-bound access token requires an HTTP request context",
992
1153
  error: "invalid_token"
@@ -1023,7 +1184,7 @@ async function userInfoEndpoint(ctx, opts) {
1023
1184
  error_description: "user not found",
1024
1185
  error: "invalid_request"
1025
1186
  });
1026
- const baseUserClaims = userNormalClaims(user, scopes ?? []);
1187
+ const baseUserClaims = userNormalClaims(user, scopes ?? [], requestedClaims);
1027
1188
  const clientId = jwt.client_id ?? jwt.azp;
1028
1189
  const client = clientId && (opts.pairwiseSecret || hasUserInfoClaimExtension(opts)) ? await getClient(ctx, opts, clientId) : void 0;
1029
1190
  if (opts.pairwiseSecret && client) baseUserClaims.sub = await resolveSubjectIdentifier(user.id, client, opts);
@@ -1033,12 +1194,14 @@ async function userInfoEndpoint(ctx, opts) {
1033
1194
  user,
1034
1195
  scopes,
1035
1196
  jwt,
1036
- client: client ?? void 0
1197
+ client: client ?? void 0,
1198
+ requestedClaims
1037
1199
  }) : {};
1038
1200
  const additionalInfoUserClaims = opts.customUserInfoClaims && scopes?.length ? await opts.customUserInfoClaims({
1039
1201
  user,
1040
1202
  scopes,
1041
- jwt
1203
+ jwt,
1204
+ requestedClaims
1042
1205
  }) : {};
1043
1206
  return {
1044
1207
  ...baseUserClaims,
@@ -1049,6 +1212,7 @@ async function userInfoEndpoint(ctx, opts) {
1049
1212
  }
1050
1213
  //#endregion
1051
1214
  //#region src/token.ts
1215
+ const ID_TOKEN_SCOPE_CLAIM_GUARDS = Object.fromEntries(STANDARD_CLAIM_NAMES.map((name) => [name, void 0]));
1052
1216
  /**
1053
1217
  * Token presentation scheme implied by a confirmation: a DPoP key thumbprint
1054
1218
  * (`jkt`) yields `"DPoP"`; any other confirmation (including mTLS `x5t#S256`)
@@ -1183,24 +1347,22 @@ async function computeOidcHash(token, signingAlg) {
1183
1347
  async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken, extraClaims) {
1184
1348
  const iat = Math.floor(Date.now() / 1e3);
1185
1349
  const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
1186
- const userClaims = userNormalClaims(user, scopes);
1187
1350
  const resolvedSub = await resolveSubjectIdentifier(user.id, client, opts);
1188
1351
  const authTimeSec = authTime != null ? Math.floor(authTime.getTime() / 1e3) : void 0;
1189
- const acr = "urn:mace:incommon:iap:bronze";
1190
- const customClaims = opts.customIdTokenClaims ? await opts.customIdTokenClaims({
1352
+ const customClaims = stripReservedIdTokenClaims(opts.customIdTokenClaims ? await opts.customIdTokenClaims({
1191
1353
  user,
1192
1354
  scopes,
1193
1355
  metadata: parseClientMetadata(client.metadata)
1194
- }) : {};
1356
+ }) : void 0);
1195
1357
  const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
1196
1358
  const resolvedKey = !opts.disableJwtPlugin && !jwtPluginOptions?.jwt?.sign ? await resolveSigningKey(ctx, jwtPluginOptions) : void 0;
1197
1359
  const signingAlg = opts.disableJwtPlugin ? "HS256" : resolvedKey?.alg ?? jwtPluginOptions?.jwks?.keyPairConfig?.alg;
1198
1360
  const atHash = accessToken ? await computeOidcHash(accessToken, signingAlg) : void 0;
1199
1361
  const emitSid = Boolean(client.enableEndSession || client.backchannelLogoutUri);
1200
1362
  const payload = {
1201
- ...userClaims,
1363
+ ...ID_TOKEN_SCOPE_CLAIM_GUARDS,
1202
1364
  auth_time: authTimeSec,
1203
- acr,
1365
+ acr: "0",
1204
1366
  ...customClaims,
1205
1367
  at_hash: atHash,
1206
1368
  iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
@@ -1211,7 +1373,8 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
1211
1373
  exp,
1212
1374
  sid: emitSid ? sessionId : void 0
1213
1375
  };
1214
- Object.assign(payload, pickClaims(extraClaims, payload));
1376
+ const additiveClaims = stripReservedIdTokenClaims(extraClaims);
1377
+ Object.assign(payload, pickClaims(additiveClaims, payload));
1215
1378
  if (opts.disableJwtPlugin && !client.clientSecret) return;
1216
1379
  const idToken = opts.disableJwtPlugin ? await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(new TextEncoder().encode(await decryptStoredClientSecret(ctx, opts.storeClientSecret, client.clientSecret))) : await signJWT(ctx, {
1217
1380
  options: jwtPluginOptions,
@@ -1246,7 +1409,7 @@ async function decodeRefreshToken(opts, token) {
1246
1409
  });
1247
1410
  return opts.formatRefreshToken?.decrypt ? opts.formatRefreshToken?.decrypt(token) : { token };
1248
1411
  }
1249
- async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, refreshId, confirmation) {
1412
+ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, authorizationCodeId, refreshId, confirmation, requestedUserInfoClaims) {
1250
1413
  const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
1251
1414
  const exp = payload?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
1252
1415
  const token = opts.generateOpaqueAccessToken ? await opts.generateOpaqueAccessToken() : generateRandomString(32, "A-Z", "a-z");
@@ -1258,9 +1421,11 @@ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload,
1258
1421
  sessionId: payload?.sid,
1259
1422
  userId: user?.id,
1260
1423
  referenceId,
1424
+ authorizationCodeId,
1261
1425
  resources,
1262
1426
  refreshId,
1263
1427
  confirmation,
1428
+ requestedUserInfoClaims: requestedUserInfoClaims?.length ? requestedUserInfoClaims : void 0,
1264
1429
  scopes,
1265
1430
  createdAt: /* @__PURE__ */ new Date(iat * 1e3),
1266
1431
  expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
@@ -1315,7 +1480,24 @@ async function invalidateRefreshFamily(ctx, clientId, userId) {
1315
1480
  }]
1316
1481
  });
1317
1482
  }
1318
- async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, payload, originalRefresh, authTime, resources, confirmation) {
1483
+ async function revokeTokensIssuedForAuthorizationCode(ctx, authorizationCodeId) {
1484
+ const deleteIssuedTokens = async (model) => {
1485
+ try {
1486
+ await ctx.context.adapter.deleteMany({
1487
+ model,
1488
+ where: [{
1489
+ field: "authorizationCodeId",
1490
+ value: authorizationCodeId
1491
+ }]
1492
+ });
1493
+ } catch (error) {
1494
+ ctx.context.logger.error("authorization code replay cleanup failed", error);
1495
+ }
1496
+ };
1497
+ await deleteIssuedTokens("oauthAccessToken");
1498
+ await deleteIssuedTokens("oauthRefreshToken");
1499
+ }
1500
+ async function createRefreshToken(ctx, opts, user, referenceId, authorizationCodeId, client, scopes, payload, originalRefresh, authTime, resources, confirmation, requestedUserInfoClaims) {
1319
1501
  const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
1320
1502
  const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
1321
1503
  const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
@@ -1326,8 +1508,10 @@ async function createRefreshToken(ctx, opts, user, referenceId, client, scopes,
1326
1508
  sessionId,
1327
1509
  userId: user.id,
1328
1510
  referenceId,
1511
+ authorizationCodeId,
1329
1512
  authTime,
1330
1513
  confirmation,
1514
+ requestedUserInfoClaims: requestedUserInfoClaims?.length ? requestedUserInfoClaims : void 0,
1331
1515
  scopes,
1332
1516
  resources,
1333
1517
  createdAt: /* @__PURE__ */ new Date(iat * 1e3),
@@ -1340,6 +1524,13 @@ async function createRefreshToken(ctx, opts, user, referenceId, client, scopes,
1340
1524
  })).id,
1341
1525
  token: await encodeRefreshToken(opts, token, sessionId)
1342
1526
  };
1527
+ const rotatedAt = /* @__PURE__ */ new Date(iat * 1e3);
1528
+ const rotationUpdate = {
1529
+ revoked: rotatedAt,
1530
+ rotatedAt
1531
+ };
1532
+ const reuseInterval = opts.refreshTokenReuseInterval ?? 0;
1533
+ if (reuseInterval > 0) rotationUpdate.rotationReplayExpiresAt = /* @__PURE__ */ new Date((iat + reuseInterval) * 1e3);
1343
1534
  if (!await ctx.context.adapter.incrementOne({
1344
1535
  model: "oauthRefreshToken",
1345
1536
  where: [{
@@ -1351,7 +1542,7 @@ async function createRefreshToken(ctx, opts, user, referenceId, client, scopes,
1351
1542
  value: null
1352
1543
  }],
1353
1544
  increment: {},
1354
- set: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
1545
+ set: rotationUpdate
1355
1546
  })) throw new APIError("BAD_REQUEST", {
1356
1547
  error_description: "invalid refresh token",
1357
1548
  error: "invalid_grant"
@@ -1420,6 +1611,126 @@ async function resolveDpopTokenBinding(ctx, opts, params) {
1420
1611
  throw error;
1421
1612
  }
1422
1613
  }
1614
+ function normalizeReplayValues(values) {
1615
+ return values ? [...new Set(values)].sort() : void 0;
1616
+ }
1617
+ function sameReplayValues(left, right) {
1618
+ const normalizedLeft = normalizeReplayValues(left);
1619
+ const normalizedRight = normalizeReplayValues(right);
1620
+ if (normalizedLeft === void 0 || normalizedRight === void 0) return normalizedLeft === normalizedRight;
1621
+ if (normalizedLeft.length !== normalizedRight.length) return false;
1622
+ return normalizedLeft.every((value, index) => value === normalizedRight[index]);
1623
+ }
1624
+ function confirmationReplayKey(confirmation) {
1625
+ if (!confirmation) return;
1626
+ if ("jkt" in confirmation) return `jkt:${confirmation.jkt}`;
1627
+ return `x5t#S256:${confirmation["x5t#S256"]}`;
1628
+ }
1629
+ function sameConfirmation(left, right) {
1630
+ return confirmationReplayKey(left) === confirmationReplayKey(right);
1631
+ }
1632
+ function isConfirmation(value) {
1633
+ if (!value || typeof value !== "object") return false;
1634
+ const confirmation = value;
1635
+ return typeof confirmation.jkt === "string" && confirmation["x5t#S256"] === void 0 || typeof confirmation["x5t#S256"] === "string" && confirmation.jkt === void 0;
1636
+ }
1637
+ function isTokenType(value) {
1638
+ return value === "Bearer" || value === "DPoP";
1639
+ }
1640
+ function isOAuthTokenResponse(value) {
1641
+ if (!value || typeof value !== "object") return false;
1642
+ const response = value;
1643
+ return typeof response.access_token === "string" && typeof response.expires_in === "number" && typeof response.expires_at === "number" && isTokenType(response.token_type) && typeof response.scope === "string" && (response.refresh_token === void 0 || typeof response.refresh_token === "string") && (response.id_token === void 0 || typeof response.id_token === "string");
1644
+ }
1645
+ function isRefreshTokenRotationReplay(value) {
1646
+ if (!value || typeof value !== "object") return false;
1647
+ const replay = value;
1648
+ const request = replay.request;
1649
+ return !!request && Array.isArray(request.effectiveScopes) && request.effectiveScopes.every((scope) => typeof scope === "string") && (request.requestedResources === void 0 || Array.isArray(request.requestedResources) && request.requestedResources.every((resource) => typeof resource === "string")) && (request.confirmation === void 0 || isConfirmation(request.confirmation)) && isOAuthTokenResponse(replay.response);
1650
+ }
1651
+ function buildRefreshTokenRotationReplayRequest(params) {
1652
+ const requestedResources = normalizeReplayValues(params.requestedResources);
1653
+ return {
1654
+ effectiveScopes: normalizeReplayValues(params.effectiveScopes) ?? [],
1655
+ ...requestedResources ? { requestedResources } : {},
1656
+ ...params.confirmation ? { confirmation: params.confirmation } : {}
1657
+ };
1658
+ }
1659
+ function sameRefreshTokenRotationReplayRequest(left, right) {
1660
+ return sameReplayValues(left.effectiveScopes, right.effectiveScopes) && sameReplayValues(left.requestedResources, right.requestedResources) && sameConfirmation(left.confirmation, right.confirmation);
1661
+ }
1662
+ async function encryptRefreshTokenRotationReplay(ctx, replay) {
1663
+ return symmetricEncrypt({
1664
+ key: ctx.context.secretConfig,
1665
+ data: JSON.stringify(replay)
1666
+ });
1667
+ }
1668
+ async function decryptRefreshTokenRotationReplay(ctx, value) {
1669
+ const decrypted = await symmetricDecrypt({
1670
+ key: ctx.context.secretConfig,
1671
+ data: value
1672
+ });
1673
+ const parsed = JSON.parse(decrypted);
1674
+ if (!isRefreshTokenRotationReplay(parsed)) return;
1675
+ return parsed;
1676
+ }
1677
+ function isWithinRefreshTokenReuseInterval(refreshToken) {
1678
+ return !!refreshToken.rotatedAt && !!refreshToken.rotationReplayExpiresAt && refreshToken.rotationReplayExpiresAt >= /* @__PURE__ */ new Date();
1679
+ }
1680
+ async function storeRefreshTokenRotationReplay(ctx, opts, refreshToken, request, response) {
1681
+ if ((opts.refreshTokenReuseInterval ?? 0) <= 0) return;
1682
+ await ctx.context.adapter.update({
1683
+ model: "oauthRefreshToken",
1684
+ where: [{
1685
+ field: "id",
1686
+ value: refreshToken.id
1687
+ }],
1688
+ update: { rotationReplayResponse: await encryptRefreshTokenRotationReplay(ctx, {
1689
+ request,
1690
+ response
1691
+ }) }
1692
+ });
1693
+ }
1694
+ async function getRefreshTokenRotationReplay(ctx, refreshToken, request) {
1695
+ if (!isWithinRefreshTokenReuseInterval(refreshToken)) return;
1696
+ if (!refreshToken.rotationReplayResponse) return;
1697
+ try {
1698
+ const replay = await decryptRefreshTokenRotationReplay(ctx, refreshToken.rotationReplayResponse);
1699
+ if (!replay || !sameRefreshTokenRotationReplayRequest(replay.request, request)) return;
1700
+ return replay.response;
1701
+ } catch (error) {
1702
+ ctx.context.logger.error("refresh token rotation replay failed", error);
1703
+ return;
1704
+ }
1705
+ }
1706
+ async function resolveRefreshTokenRotationReplayRequest(ctx, opts, params) {
1707
+ const iat = Math.floor(Date.now() / 1e3);
1708
+ const grantIssuance = await resolveResourceGrantIssuance(ctx, opts, {
1709
+ clientId: params.client.clientId,
1710
+ requestedScopes: params.scopes,
1711
+ resources: params.resources,
1712
+ refreshToken: params.refreshToken,
1713
+ iat,
1714
+ scopeExpiresAtSeconds: iat + (opts.accessTokenExpiresIn ?? 3600)
1715
+ });
1716
+ const confirmation = await resolveDpopTokenBinding(ctx, opts, {
1717
+ client: params.client,
1718
+ grantIssuance,
1719
+ refreshToken: params.refreshToken
1720
+ });
1721
+ return buildRefreshTokenRotationReplayRequest({
1722
+ effectiveScopes: grantIssuance.effectiveScopes,
1723
+ requestedResources: params.resources,
1724
+ confirmation
1725
+ });
1726
+ }
1727
+ function replayRefreshTokenRotationResponse(ctx, response) {
1728
+ const now = Math.floor(Date.now() / 1e3);
1729
+ return ctx.json({
1730
+ ...response,
1731
+ expires_in: Math.max(0, response.expires_at - now)
1732
+ });
1733
+ }
1423
1734
  async function createUserTokens(ctx, opts, params) {
1424
1735
  const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, verificationValue } = params;
1425
1736
  const iat = Math.floor(Date.now() / 1e3);
@@ -1473,11 +1784,12 @@ async function createUserTokens(ctx, opts, params) {
1473
1784
  verificationValue,
1474
1785
  refreshToken: existingRefreshToken
1475
1786
  });
1476
- const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, client, effectiveScopes, {
1787
+ const requestedUserInfoClaims = params.requestedUserInfoClaims ?? existingRefreshToken?.requestedUserInfoClaims ?? [];
1788
+ const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, params.authorizationCodeId, client, effectiveScopes, {
1477
1789
  iat,
1478
1790
  exp: refreshTokenExp,
1479
1791
  sid: sessionId
1480
- }, existingRefreshToken, authTime, refreshResources, confirmation) : void 0;
1792
+ }, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0;
1481
1793
  const accessTokenClaims = isJwtAccessToken ? await resolveAccessTokenClaims({
1482
1794
  ctx,
1483
1795
  opts,
@@ -1504,13 +1816,13 @@ async function createUserTokens(ctx, opts, params) {
1504
1816
  iat,
1505
1817
  exp,
1506
1818
  sid: sessionId
1507
- }, params?.resources, referenceId, earlyRefreshToken?.id, confirmation), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, effectiveScopes, {
1819
+ }, params?.resources, referenceId, params.authorizationCodeId, earlyRefreshToken?.id, confirmation, requestedUserInfoClaims), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, params.authorizationCodeId, client, effectiveScopes, {
1508
1820
  iat,
1509
1821
  exp: refreshTokenExp,
1510
1822
  sid: sessionId
1511
- }, existingRefreshToken, authTime, refreshResources, confirmation) : void 0]);
1823
+ }, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0]);
1512
1824
  const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, effectiveScopes, nonce, sessionId, authTime, accessToken, additionalIdTokenClaims) : void 0;
1513
- return ctx.json({
1825
+ const responseBody = {
1514
1826
  ...customFields,
1515
1827
  ...params.tokenResponse ?? {},
1516
1828
  access_token: accessToken,
@@ -1520,37 +1832,61 @@ async function createUserTokens(ctx, opts, params) {
1520
1832
  refresh_token: refreshToken?.token,
1521
1833
  scope: effectiveScopes.join(" "),
1522
1834
  id_token: idToken
1523
- });
1835
+ };
1836
+ if (existingRefreshToken?.id && refreshToken?.id) try {
1837
+ await storeRefreshTokenRotationReplay(ctx, opts, existingRefreshToken, buildRefreshTokenRotationReplayRequest({
1838
+ effectiveScopes,
1839
+ requestedResources: params.resources,
1840
+ confirmation
1841
+ }), responseBody);
1842
+ } catch (error) {
1843
+ ctx.context.logger.error("failed to store refresh token rotation replay", error);
1844
+ }
1845
+ return ctx.json(responseBody);
1524
1846
  }
1525
1847
  /** Checks verification value */
1526
1848
  async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resource) {
1527
- const verification = await ctx.context.internalAdapter.consumeVerificationValue(await storeToken(opts.storeTokens, code, "authorization_code"));
1528
- if (!verification) throw new APIError("UNAUTHORIZED", {
1529
- error_description: "invalid code",
1530
- error: "invalid_grant"
1531
- });
1849
+ const authorizationCodeId = await storeToken(opts.storeTokens, code, "authorization_code");
1850
+ const verification = await ctx.context.internalAdapter.consumeVerificationValue(authorizationCodeId);
1851
+ if (!verification) {
1852
+ await revokeTokensIssuedForAuthorizationCode(ctx, authorizationCodeId);
1853
+ throw new APIError("BAD_REQUEST", {
1854
+ error_description: "invalid code",
1855
+ error: "invalid_grant"
1856
+ });
1857
+ }
1532
1858
  let rawValue;
1533
1859
  try {
1534
1860
  rawValue = JSON.parse(verification.value);
1535
1861
  } catch {
1536
- throw new APIError("UNAUTHORIZED", {
1862
+ throw new APIError("BAD_REQUEST", {
1537
1863
  error_description: "malformed verification value",
1538
1864
  error: "invalid_grant"
1539
1865
  });
1540
1866
  }
1541
1867
  const parsed = verificationValueSchema.safeParse(rawValue);
1542
- if (!parsed.success) throw new APIError("UNAUTHORIZED", {
1868
+ if (!parsed.success) throw new APIError("BAD_REQUEST", {
1543
1869
  error_description: "malformed verification value",
1544
1870
  error: "invalid_grant"
1545
1871
  });
1546
1872
  const verificationValue = parsed.data;
1547
- if (verificationValue.query.client_id !== client_id) throw new APIError("UNAUTHORIZED", {
1873
+ if (verificationValue.query.client_id !== client_id) throw new APIError("BAD_REQUEST", {
1548
1874
  error_description: "invalid client_id",
1549
- error: "invalid_client"
1875
+ error: "invalid_grant"
1550
1876
  });
1551
- if (verificationValue.query?.redirect_uri && verificationValue.query?.redirect_uri !== redirect_uri) throw new APIError("BAD_REQUEST", {
1877
+ const boundRedirectUri = verificationValue.query.redirect_uri;
1878
+ if (boundRedirectUri) {
1879
+ if (!redirect_uri) throw new APIError("BAD_REQUEST", {
1880
+ error_description: "redirect_uri is required",
1881
+ error: "invalid_request"
1882
+ });
1883
+ if (boundRedirectUri !== redirect_uri) throw new APIError("BAD_REQUEST", {
1884
+ error_description: "redirect_uri mismatch",
1885
+ error: "invalid_grant"
1886
+ });
1887
+ } else if (redirect_uri) throw new APIError("BAD_REQUEST", {
1552
1888
  error_description: "redirect_uri mismatch",
1553
- error: "invalid_request"
1889
+ error: "invalid_grant"
1554
1890
  });
1555
1891
  const storedResources = toResourceList(verificationValue.resource) ?? toResourceList(verificationValue.query.resource);
1556
1892
  const effectiveResources = resource ?? storedResources;
@@ -1565,7 +1901,8 @@ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri,
1565
1901
  return {
1566
1902
  verificationValue,
1567
1903
  effectiveResources,
1568
- authorizedResources: storedResources
1904
+ authorizedResources: storedResources,
1905
+ authorizationCodeId
1569
1906
  };
1570
1907
  }
1571
1908
  /**
@@ -1583,10 +1920,6 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1583
1920
  error_description: "code is required",
1584
1921
  error: "invalid_request"
1585
1922
  });
1586
- if (!redirect_uri) throw new APIError("BAD_REQUEST", {
1587
- error_description: "redirect_uri is required",
1588
- error: "invalid_request"
1589
- });
1590
1923
  const isAuthCodeWithSecret = client_id && client_secret;
1591
1924
  const isAuthCodeWithPkce = client_id && code && code_verifier;
1592
1925
  if (!isAuthCodeWithSecret && !isAuthCodeWithPkce && !preVerified) throw new APIError("BAD_REQUEST", {
@@ -1594,7 +1927,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1594
1927
  error: "invalid_request"
1595
1928
  });
1596
1929
  /** Get and check Verification Value */
1597
- const { verificationValue, effectiveResources, authorizedResources } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
1930
+ const { verificationValue, effectiveResources, authorizedResources, authorizationCodeId } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
1598
1931
  const scopes = verificationValue.query.scope?.split(" ");
1599
1932
  if (!scopes) throw new APIError("INTERNAL_SERVER_ERROR", {
1600
1933
  error_description: "verification scope unset",
@@ -1602,7 +1935,10 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1602
1935
  });
1603
1936
  /** Verify Client */
1604
1937
  const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerified, "authorization_code", authMethod);
1605
- if (isPKCERequired(client, (verificationValue.query?.scope)?.split(" ") || [])) {
1938
+ if (isPKCERequired(client, {
1939
+ scopes: (verificationValue.query?.scope)?.split(" ") || [],
1940
+ nonce: verificationValue.query?.nonce
1941
+ })) {
1606
1942
  if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
1607
1943
  error_description: "PKCE is required for this client",
1608
1944
  error: "invalid_request"
@@ -1650,6 +1986,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1650
1986
  error: "invalid_request"
1651
1987
  });
1652
1988
  const authTime = verificationValue.authTime != null ? normalizeTimestampValue(verificationValue.authTime) : resolveSessionAuthTime(session);
1989
+ const requestedUserInfoClaims = getRequestedUserInfoClaims(verificationValue.query.claims, getSupportedClaims(opts));
1653
1990
  return createUserTokens(ctx, opts, {
1654
1991
  client,
1655
1992
  scopes: verificationValue.query.scope?.split(" ") ?? [],
@@ -1660,6 +1997,8 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1660
1997
  nonce: verificationValue.query?.nonce,
1661
1998
  authTime,
1662
1999
  verificationValue,
2000
+ authorizationCodeId,
2001
+ requestedUserInfoClaims,
1663
2002
  resources: effectiveResources,
1664
2003
  originalResources: authorizedResources
1665
2004
  });
@@ -1739,20 +2078,13 @@ async function handleRefreshTokenGrant(ctx, opts) {
1739
2078
  error: "invalid_grant"
1740
2079
  });
1741
2080
  if (refreshToken.clientId !== client_id) throw new APIError("BAD_REQUEST", {
1742
- error_description: "invalid client_id",
1743
- error: "invalid_client"
2081
+ error_description: "invalid refresh token",
2082
+ error: "invalid_grant"
1744
2083
  });
1745
2084
  if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
1746
2085
  error_description: "invalid refresh token",
1747
2086
  error: "invalid_grant"
1748
2087
  });
1749
- if (refreshToken.revoked) {
1750
- await invalidateRefreshFamily(ctx, client_id, refreshToken.userId);
1751
- throw new APIError("BAD_REQUEST", {
1752
- error_description: "invalid refresh token",
1753
- error: "invalid_grant"
1754
- });
1755
- }
1756
2088
  if (resources && refreshToken.resources && !resources.every((v) => refreshToken.resources?.includes(v))) throw new APIError("BAD_REQUEST", {
1757
2089
  error_description: "requested resource invalid",
1758
2090
  error: "invalid_target"
@@ -1767,6 +2099,26 @@ async function handleRefreshTokenGrant(ctx, opts) {
1767
2099
  });
1768
2100
  }
1769
2101
  const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes, preVerified, "refresh_token", authMethod);
2102
+ if (refreshToken.revoked) {
2103
+ if (isWithinRefreshTokenReuseInterval(refreshToken)) {
2104
+ const replay = await getRefreshTokenRotationReplay(ctx, refreshToken, await resolveRefreshTokenRotationReplayRequest(ctx, opts, {
2105
+ client,
2106
+ refreshToken,
2107
+ scopes: requestedScopes ?? scopes,
2108
+ resources: resources ?? refreshToken.resources
2109
+ }));
2110
+ if (replay) return replayRefreshTokenRotationResponse(ctx, replay);
2111
+ throw new APIError("BAD_REQUEST", {
2112
+ error_description: "invalid refresh token",
2113
+ error: "invalid_grant"
2114
+ });
2115
+ }
2116
+ await invalidateRefreshFamily(ctx, client_id, refreshToken.userId);
2117
+ throw new APIError("BAD_REQUEST", {
2118
+ error_description: "invalid refresh token",
2119
+ error: "invalid_grant"
2120
+ });
2121
+ }
1770
2122
  const user = await ctx.context.internalAdapter.findUserById(refreshToken.userId);
1771
2123
  if (!user) throw new APIError("BAD_REQUEST", {
1772
2124
  error_description: "user not found",
@@ -1779,10 +2131,12 @@ async function handleRefreshTokenGrant(ctx, opts) {
1779
2131
  user,
1780
2132
  grantType: "refresh_token",
1781
2133
  referenceId: refreshToken.referenceId,
2134
+ authorizationCodeId: refreshToken.authorizationCodeId,
1782
2135
  sessionId: refreshToken.sessionId,
1783
2136
  refreshToken,
1784
2137
  resources: resources ?? refreshToken.resources,
1785
- authTime
2138
+ authTime,
2139
+ requestedUserInfoClaims: refreshToken.requestedUserInfoClaims
1786
2140
  });
1787
2141
  }
1788
2142
  //#endregion
@@ -1790,10 +2144,17 @@ async function handleRefreshTokenGrant(ctx, opts) {
1790
2144
  var introspect_exports = /* @__PURE__ */ __exportAll({
1791
2145
  introspectEndpoint: () => introspectEndpoint,
1792
2146
  requireActiveAccessToken: () => requireActiveAccessToken,
2147
+ requireActiveAccessTokenWithClaims: () => requireActiveAccessTokenWithClaims,
1793
2148
  validateAccessToken: () => validateAccessToken
1794
2149
  });
1795
2150
  const INVALID_ACCESS_TOKEN_ERROR_DESCRIPTION = "Invalid access token";
1796
2151
  const INVALID_ACCESS_TOKEN_WWW_AUTHENTICATE = `Bearer error="invalid_token", error_description="${INVALID_ACCESS_TOKEN_ERROR_DESCRIPTION}"`;
2152
+ function inactiveAccessToken() {
2153
+ return {
2154
+ payload: { active: false },
2155
+ requestedUserInfoClaims: []
2156
+ };
2157
+ }
1797
2158
  /**
1798
2159
  * IMPORTANT NOTES:
1799
2160
  * Introspection follows RFC7662
@@ -1903,18 +2264,18 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
1903
2264
  error_description: "opaque access token not found",
1904
2265
  error: "invalid_token"
1905
2266
  });
1906
- if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
1907
- if (accessToken.revoked) return { active: false };
2267
+ if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return inactiveAccessToken();
2268
+ if (accessToken.revoked) return inactiveAccessToken();
1908
2269
  const resources = Array.isArray(accessToken.resources) ? accessToken.resources : void 0;
1909
2270
  let client;
1910
2271
  if (accessToken.clientId) {
1911
2272
  client = await getClient(ctx, opts, accessToken.clientId);
1912
- if (!client || client?.disabled) return { active: false };
2273
+ if (!client || client?.disabled) return inactiveAccessToken();
1913
2274
  if (!await isIntrospectionAuthorized(ctx, opts, {
1914
2275
  introspectingClientId: clientId,
1915
2276
  issuerClientId: accessToken.clientId,
1916
2277
  audienceResources: resources ?? []
1917
- })) return { active: false };
2278
+ })) return inactiveAccessToken();
1918
2279
  }
1919
2280
  const sessionId = accessToken.sessionId ?? void 0;
1920
2281
  if (sessionId) {
@@ -1925,12 +2286,12 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
1925
2286
  value: sessionId
1926
2287
  }]
1927
2288
  });
1928
- if (!session || session.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
2289
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) return inactiveAccessToken();
1929
2290
  }
1930
2291
  let user;
1931
2292
  if (accessToken.userId) user = await ctx.context.internalAdapter.findUserById(accessToken?.userId);
1932
2293
  const userInfoEndpoint = `${ctx.context.baseURL}/oauth2/userinfo`;
1933
- if (resources?.length && !await isAudienceClaimAllowed(ctx, opts, resources, [userInfoEndpoint])) return { active: false };
2294
+ if (resources?.length && !await isAudienceClaimAllowed(ctx, opts, resources, [userInfoEndpoint])) return inactiveAccessToken();
1934
2295
  const audienceClaim = resources ? [...resources] : void 0;
1935
2296
  if (audienceClaim?.length && accessToken.scopes?.includes("openid")) {
1936
2297
  if (!audienceClaim.includes(userInfoEndpoint)) audienceClaim.push(userInfoEndpoint);
@@ -1952,19 +2313,22 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
1952
2313
  }) : {};
1953
2314
  const jwtPluginOptions = (opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options;
1954
2315
  return {
1955
- ...accessTokenClaims,
1956
- active: true,
1957
- iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
1958
- aud: toAudienceClaim(audienceClaim),
1959
- client_id: accessToken.clientId,
1960
- azp: accessToken.clientId,
1961
- sub: user?.id,
1962
- sid: sessionId,
1963
- exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
1964
- iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
1965
- scope: accessToken.scopes?.join(" "),
1966
- token_type: confirmationTokenType(accessToken.confirmation),
1967
- ...accessToken.confirmation ? { cnf: accessToken.confirmation } : {}
2316
+ payload: {
2317
+ ...accessTokenClaims,
2318
+ active: true,
2319
+ iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
2320
+ aud: toAudienceClaim(audienceClaim),
2321
+ client_id: accessToken.clientId,
2322
+ azp: accessToken.clientId,
2323
+ sub: user?.id,
2324
+ sid: sessionId,
2325
+ exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
2326
+ iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
2327
+ scope: accessToken.scopes?.join(" "),
2328
+ token_type: confirmationTokenType(accessToken.confirmation),
2329
+ ...accessToken.confirmation ? { cnf: accessToken.confirmation } : {}
2330
+ },
2331
+ requestedUserInfoClaims: accessToken.requestedUserInfoClaims ?? []
1968
2332
  };
1969
2333
  }
1970
2334
  /**
@@ -2023,16 +2387,17 @@ function isInactiveTokenError(error) {
2023
2387
  return error.status === "BAD_REQUEST" || error.body?.error === "invalid_token";
2024
2388
  }
2025
2389
  /**
2026
- * We don't know the access token format so we try to validate it
2027
- * as a JWT first, then as an opaque token.
2028
- *
2029
- * @returns RFC7662 introspection format
2030
- *
2031
- * @internal
2390
+ * We don't know the access token format so we try to validate it as a JWT
2391
+ * first, then as an opaque token. Returns the RFC 7662 introspection payload
2392
+ * alongside the per-issuance UserInfo claims the opaque row persisted (empty for
2393
+ * a JWT access token, which resolves UserInfo claims by scope).
2032
2394
  */
2033
- async function validateAccessToken(ctx, opts, token, clientId) {
2395
+ async function resolveAccessTokenValidation(ctx, opts, token, clientId) {
2034
2396
  try {
2035
- return await validateJwtAccessToken(ctx, opts, token, clientId);
2397
+ return {
2398
+ payload: await validateJwtAccessToken(ctx, opts, token, clientId),
2399
+ requestedUserInfoClaims: []
2400
+ };
2036
2401
  } catch (err) {
2037
2402
  if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
2038
2403
  else throw new Error(err);
@@ -2045,12 +2410,35 @@ async function validateAccessToken(ctx, opts, token, clientId) {
2045
2410
  }
2046
2411
  throw createInvalidAccessTokenError();
2047
2412
  }
2413
+ /**
2414
+ * Validates an access token (JWT or opaque) and returns its RFC 7662
2415
+ * introspection-format payload.
2416
+ *
2417
+ * @internal
2418
+ */
2419
+ async function validateAccessToken(ctx, opts, token, clientId) {
2420
+ return (await resolveAccessTokenValidation(ctx, opts, token, clientId)).payload;
2421
+ }
2048
2422
  async function requireActiveAccessToken(ctx, opts, token, clientId) {
2049
2423
  const payload = await validateAccessToken(ctx, opts, token, clientId);
2050
2424
  if (payload.active) return payload;
2051
2425
  throw createInvalidAccessTokenError();
2052
2426
  }
2053
2427
  /**
2428
+ * UserInfo entry point: validates the access token, requires it active, and
2429
+ * returns the introspection payload together with the OIDC `claims.userinfo`
2430
+ * names persisted for the token. Opaque tokens carry the names on their row;
2431
+ * JWT access tokens return an empty list and resolve UserInfo claims by scope.
2432
+ */
2433
+ async function requireActiveAccessTokenWithClaims(ctx, opts, token, clientId) {
2434
+ const { payload, requestedUserInfoClaims } = await resolveAccessTokenValidation(ctx, opts, token, clientId);
2435
+ if (payload.active) return {
2436
+ payload,
2437
+ requestedUserInfoClaims
2438
+ };
2439
+ throw createInvalidAccessTokenError();
2440
+ }
2441
+ /**
2054
2442
  * Resolves pairwise sub on an introspection payload.
2055
2443
  * Applied at the presentation layer so internal validation functions
2056
2444
  * keep real user.id (needed for user lookup in /userinfo).
@@ -2116,4 +2504,4 @@ async function introspectEndpoint(ctx, opts) {
2116
2504
  }
2117
2505
  }
2118
2506
  //#endregion
2119
- export { invalidateResourceCache as _, invalidateRefreshFamily as a, resolveResourcePolicy as b, ResourceUriSchema as c, clientRegistrationRequestSchema as d, JWS_ALGORITHMS as f, getResource as g, extractRepeatedResourceFromForm as h, getOAuthProviderApi as i, SafeUrlSchema as l, buildClientResourceLinkId as m, introspect_exports as n, tokenEndpoint as o, assertIdentifierValid as p, decodeRefreshToken as r, userInfoEndpoint as s, introspectEndpoint as t, authorizationQuerySchema as u, isAudienceClaimAllowed as v, seedResources as x, logEnforcePerClientResourcesResolution as y };
2507
+ export { STANDARD_CLAIM_NAMES as C, getRequestedUserInfoClaims as D, filterClaimsRequestUserInfoClaims as E, STANDARD_CLAIMS as S, claimsRequestParameterSchema as T, invalidateResourceCache as _, invalidateRefreshFamily as a, resolveResourcePolicy as b, ResourceUriSchema as c, clientRegistrationRequestSchema as d, JWS_ALGORITHMS as f, getResource as g, extractRepeatedResourceFromForm as h, getOAuthProviderApi as i, SafeUrlSchema as l, buildClientResourceLinkId as m, introspect_exports as n, tokenEndpoint as o, assertIdentifierValid as p, decodeRefreshToken as r, userInfoEndpoint as s, introspectEndpoint as t, authorizationQuerySchema as u, isAudienceClaimAllowed as v, getSupportedClaims as w, seedResources as x, logEnforcePerClientResourcesResolution as y };