@better-auth/oauth-provider 1.7.0-beta.7 → 1.7.0-beta.9

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,5 +1,5 @@
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
4
  import { generateRandomString } from "better-auth/crypto";
5
5
  import { APIError as APIError$1 } from "better-call";
@@ -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),
@@ -1473,11 +1657,12 @@ async function createUserTokens(ctx, opts, params) {
1473
1657
  verificationValue,
1474
1658
  refreshToken: existingRefreshToken
1475
1659
  });
1476
- const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, client, effectiveScopes, {
1660
+ const requestedUserInfoClaims = params.requestedUserInfoClaims ?? existingRefreshToken?.requestedUserInfoClaims ?? [];
1661
+ const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, params.authorizationCodeId, client, effectiveScopes, {
1477
1662
  iat,
1478
1663
  exp: refreshTokenExp,
1479
1664
  sid: sessionId
1480
- }, existingRefreshToken, authTime, refreshResources, confirmation) : void 0;
1665
+ }, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0;
1481
1666
  const accessTokenClaims = isJwtAccessToken ? await resolveAccessTokenClaims({
1482
1667
  ctx,
1483
1668
  opts,
@@ -1504,11 +1689,11 @@ async function createUserTokens(ctx, opts, params) {
1504
1689
  iat,
1505
1690
  exp,
1506
1691
  sid: sessionId
1507
- }, params?.resources, referenceId, earlyRefreshToken?.id, confirmation), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, effectiveScopes, {
1692
+ }, params?.resources, referenceId, params.authorizationCodeId, earlyRefreshToken?.id, confirmation, requestedUserInfoClaims), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, params.authorizationCodeId, client, effectiveScopes, {
1508
1693
  iat,
1509
1694
  exp: refreshTokenExp,
1510
1695
  sid: sessionId
1511
- }, existingRefreshToken, authTime, refreshResources, confirmation) : void 0]);
1696
+ }, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0]);
1512
1697
  const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, effectiveScopes, nonce, sessionId, authTime, accessToken, additionalIdTokenClaims) : void 0;
1513
1698
  return ctx.json({
1514
1699
  ...customFields,
@@ -1524,33 +1709,47 @@ async function createUserTokens(ctx, opts, params) {
1524
1709
  }
1525
1710
  /** Checks verification value */
1526
1711
  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
- });
1712
+ const authorizationCodeId = await storeToken(opts.storeTokens, code, "authorization_code");
1713
+ const verification = await ctx.context.internalAdapter.consumeVerificationValue(authorizationCodeId);
1714
+ if (!verification) {
1715
+ await revokeTokensIssuedForAuthorizationCode(ctx, authorizationCodeId);
1716
+ throw new APIError("BAD_REQUEST", {
1717
+ error_description: "invalid code",
1718
+ error: "invalid_grant"
1719
+ });
1720
+ }
1532
1721
  let rawValue;
1533
1722
  try {
1534
1723
  rawValue = JSON.parse(verification.value);
1535
1724
  } catch {
1536
- throw new APIError("UNAUTHORIZED", {
1725
+ throw new APIError("BAD_REQUEST", {
1537
1726
  error_description: "malformed verification value",
1538
1727
  error: "invalid_grant"
1539
1728
  });
1540
1729
  }
1541
1730
  const parsed = verificationValueSchema.safeParse(rawValue);
1542
- if (!parsed.success) throw new APIError("UNAUTHORIZED", {
1731
+ if (!parsed.success) throw new APIError("BAD_REQUEST", {
1543
1732
  error_description: "malformed verification value",
1544
1733
  error: "invalid_grant"
1545
1734
  });
1546
1735
  const verificationValue = parsed.data;
1547
- if (verificationValue.query.client_id !== client_id) throw new APIError("UNAUTHORIZED", {
1736
+ if (verificationValue.query.client_id !== client_id) throw new APIError("BAD_REQUEST", {
1548
1737
  error_description: "invalid client_id",
1549
- error: "invalid_client"
1738
+ error: "invalid_grant"
1550
1739
  });
1551
- if (verificationValue.query?.redirect_uri && verificationValue.query?.redirect_uri !== redirect_uri) throw new APIError("BAD_REQUEST", {
1740
+ const boundRedirectUri = verificationValue.query.redirect_uri;
1741
+ if (boundRedirectUri) {
1742
+ if (!redirect_uri) throw new APIError("BAD_REQUEST", {
1743
+ error_description: "redirect_uri is required",
1744
+ error: "invalid_request"
1745
+ });
1746
+ if (boundRedirectUri !== redirect_uri) throw new APIError("BAD_REQUEST", {
1747
+ error_description: "redirect_uri mismatch",
1748
+ error: "invalid_grant"
1749
+ });
1750
+ } else if (redirect_uri) throw new APIError("BAD_REQUEST", {
1552
1751
  error_description: "redirect_uri mismatch",
1553
- error: "invalid_request"
1752
+ error: "invalid_grant"
1554
1753
  });
1555
1754
  const storedResources = toResourceList(verificationValue.resource) ?? toResourceList(verificationValue.query.resource);
1556
1755
  const effectiveResources = resource ?? storedResources;
@@ -1565,7 +1764,8 @@ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri,
1565
1764
  return {
1566
1765
  verificationValue,
1567
1766
  effectiveResources,
1568
- authorizedResources: storedResources
1767
+ authorizedResources: storedResources,
1768
+ authorizationCodeId
1569
1769
  };
1570
1770
  }
1571
1771
  /**
@@ -1583,10 +1783,6 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1583
1783
  error_description: "code is required",
1584
1784
  error: "invalid_request"
1585
1785
  });
1586
- if (!redirect_uri) throw new APIError("BAD_REQUEST", {
1587
- error_description: "redirect_uri is required",
1588
- error: "invalid_request"
1589
- });
1590
1786
  const isAuthCodeWithSecret = client_id && client_secret;
1591
1787
  const isAuthCodeWithPkce = client_id && code && code_verifier;
1592
1788
  if (!isAuthCodeWithSecret && !isAuthCodeWithPkce && !preVerified) throw new APIError("BAD_REQUEST", {
@@ -1594,7 +1790,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1594
1790
  error: "invalid_request"
1595
1791
  });
1596
1792
  /** Get and check Verification Value */
1597
- const { verificationValue, effectiveResources, authorizedResources } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
1793
+ const { verificationValue, effectiveResources, authorizedResources, authorizationCodeId } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
1598
1794
  const scopes = verificationValue.query.scope?.split(" ");
1599
1795
  if (!scopes) throw new APIError("INTERNAL_SERVER_ERROR", {
1600
1796
  error_description: "verification scope unset",
@@ -1602,7 +1798,10 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1602
1798
  });
1603
1799
  /** Verify Client */
1604
1800
  const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerified, "authorization_code", authMethod);
1605
- if (isPKCERequired(client, (verificationValue.query?.scope)?.split(" ") || [])) {
1801
+ if (isPKCERequired(client, {
1802
+ scopes: (verificationValue.query?.scope)?.split(" ") || [],
1803
+ nonce: verificationValue.query?.nonce
1804
+ })) {
1606
1805
  if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
1607
1806
  error_description: "PKCE is required for this client",
1608
1807
  error: "invalid_request"
@@ -1650,6 +1849,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1650
1849
  error: "invalid_request"
1651
1850
  });
1652
1851
  const authTime = verificationValue.authTime != null ? normalizeTimestampValue(verificationValue.authTime) : resolveSessionAuthTime(session);
1852
+ const requestedUserInfoClaims = getRequestedUserInfoClaims(verificationValue.query.claims, getSupportedClaims(opts));
1653
1853
  return createUserTokens(ctx, opts, {
1654
1854
  client,
1655
1855
  scopes: verificationValue.query.scope?.split(" ") ?? [],
@@ -1660,6 +1860,8 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1660
1860
  nonce: verificationValue.query?.nonce,
1661
1861
  authTime,
1662
1862
  verificationValue,
1863
+ authorizationCodeId,
1864
+ requestedUserInfoClaims,
1663
1865
  resources: effectiveResources,
1664
1866
  originalResources: authorizedResources
1665
1867
  });
@@ -1739,8 +1941,8 @@ async function handleRefreshTokenGrant(ctx, opts) {
1739
1941
  error: "invalid_grant"
1740
1942
  });
1741
1943
  if (refreshToken.clientId !== client_id) throw new APIError("BAD_REQUEST", {
1742
- error_description: "invalid client_id",
1743
- error: "invalid_client"
1944
+ error_description: "invalid refresh token",
1945
+ error: "invalid_grant"
1744
1946
  });
1745
1947
  if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
1746
1948
  error_description: "invalid refresh token",
@@ -1779,10 +1981,12 @@ async function handleRefreshTokenGrant(ctx, opts) {
1779
1981
  user,
1780
1982
  grantType: "refresh_token",
1781
1983
  referenceId: refreshToken.referenceId,
1984
+ authorizationCodeId: refreshToken.authorizationCodeId,
1782
1985
  sessionId: refreshToken.sessionId,
1783
1986
  refreshToken,
1784
1987
  resources: resources ?? refreshToken.resources,
1785
- authTime
1988
+ authTime,
1989
+ requestedUserInfoClaims: refreshToken.requestedUserInfoClaims
1786
1990
  });
1787
1991
  }
1788
1992
  //#endregion
@@ -1790,10 +1994,17 @@ async function handleRefreshTokenGrant(ctx, opts) {
1790
1994
  var introspect_exports = /* @__PURE__ */ __exportAll({
1791
1995
  introspectEndpoint: () => introspectEndpoint,
1792
1996
  requireActiveAccessToken: () => requireActiveAccessToken,
1997
+ requireActiveAccessTokenWithClaims: () => requireActiveAccessTokenWithClaims,
1793
1998
  validateAccessToken: () => validateAccessToken
1794
1999
  });
1795
2000
  const INVALID_ACCESS_TOKEN_ERROR_DESCRIPTION = "Invalid access token";
1796
2001
  const INVALID_ACCESS_TOKEN_WWW_AUTHENTICATE = `Bearer error="invalid_token", error_description="${INVALID_ACCESS_TOKEN_ERROR_DESCRIPTION}"`;
2002
+ function inactiveAccessToken() {
2003
+ return {
2004
+ payload: { active: false },
2005
+ requestedUserInfoClaims: []
2006
+ };
2007
+ }
1797
2008
  /**
1798
2009
  * IMPORTANT NOTES:
1799
2010
  * Introspection follows RFC7662
@@ -1903,18 +2114,18 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
1903
2114
  error_description: "opaque access token not found",
1904
2115
  error: "invalid_token"
1905
2116
  });
1906
- if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
1907
- if (accessToken.revoked) return { active: false };
2117
+ if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return inactiveAccessToken();
2118
+ if (accessToken.revoked) return inactiveAccessToken();
1908
2119
  const resources = Array.isArray(accessToken.resources) ? accessToken.resources : void 0;
1909
2120
  let client;
1910
2121
  if (accessToken.clientId) {
1911
2122
  client = await getClient(ctx, opts, accessToken.clientId);
1912
- if (!client || client?.disabled) return { active: false };
2123
+ if (!client || client?.disabled) return inactiveAccessToken();
1913
2124
  if (!await isIntrospectionAuthorized(ctx, opts, {
1914
2125
  introspectingClientId: clientId,
1915
2126
  issuerClientId: accessToken.clientId,
1916
2127
  audienceResources: resources ?? []
1917
- })) return { active: false };
2128
+ })) return inactiveAccessToken();
1918
2129
  }
1919
2130
  const sessionId = accessToken.sessionId ?? void 0;
1920
2131
  if (sessionId) {
@@ -1925,12 +2136,12 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
1925
2136
  value: sessionId
1926
2137
  }]
1927
2138
  });
1928
- if (!session || session.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
2139
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) return inactiveAccessToken();
1929
2140
  }
1930
2141
  let user;
1931
2142
  if (accessToken.userId) user = await ctx.context.internalAdapter.findUserById(accessToken?.userId);
1932
2143
  const userInfoEndpoint = `${ctx.context.baseURL}/oauth2/userinfo`;
1933
- if (resources?.length && !await isAudienceClaimAllowed(ctx, opts, resources, [userInfoEndpoint])) return { active: false };
2144
+ if (resources?.length && !await isAudienceClaimAllowed(ctx, opts, resources, [userInfoEndpoint])) return inactiveAccessToken();
1934
2145
  const audienceClaim = resources ? [...resources] : void 0;
1935
2146
  if (audienceClaim?.length && accessToken.scopes?.includes("openid")) {
1936
2147
  if (!audienceClaim.includes(userInfoEndpoint)) audienceClaim.push(userInfoEndpoint);
@@ -1952,19 +2163,22 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
1952
2163
  }) : {};
1953
2164
  const jwtPluginOptions = (opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options;
1954
2165
  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 } : {}
2166
+ payload: {
2167
+ ...accessTokenClaims,
2168
+ active: true,
2169
+ iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
2170
+ aud: toAudienceClaim(audienceClaim),
2171
+ client_id: accessToken.clientId,
2172
+ azp: accessToken.clientId,
2173
+ sub: user?.id,
2174
+ sid: sessionId,
2175
+ exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
2176
+ iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
2177
+ scope: accessToken.scopes?.join(" "),
2178
+ token_type: confirmationTokenType(accessToken.confirmation),
2179
+ ...accessToken.confirmation ? { cnf: accessToken.confirmation } : {}
2180
+ },
2181
+ requestedUserInfoClaims: accessToken.requestedUserInfoClaims ?? []
1968
2182
  };
1969
2183
  }
1970
2184
  /**
@@ -2023,16 +2237,17 @@ function isInactiveTokenError(error) {
2023
2237
  return error.status === "BAD_REQUEST" || error.body?.error === "invalid_token";
2024
2238
  }
2025
2239
  /**
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
2240
+ * We don't know the access token format so we try to validate it as a JWT
2241
+ * first, then as an opaque token. Returns the RFC 7662 introspection payload
2242
+ * alongside the per-issuance UserInfo claims the opaque row persisted (empty for
2243
+ * a JWT access token, which resolves UserInfo claims by scope).
2032
2244
  */
2033
- async function validateAccessToken(ctx, opts, token, clientId) {
2245
+ async function resolveAccessTokenValidation(ctx, opts, token, clientId) {
2034
2246
  try {
2035
- return await validateJwtAccessToken(ctx, opts, token, clientId);
2247
+ return {
2248
+ payload: await validateJwtAccessToken(ctx, opts, token, clientId),
2249
+ requestedUserInfoClaims: []
2250
+ };
2036
2251
  } catch (err) {
2037
2252
  if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
2038
2253
  else throw new Error(err);
@@ -2045,12 +2260,35 @@ async function validateAccessToken(ctx, opts, token, clientId) {
2045
2260
  }
2046
2261
  throw createInvalidAccessTokenError();
2047
2262
  }
2263
+ /**
2264
+ * Validates an access token (JWT or opaque) and returns its RFC 7662
2265
+ * introspection-format payload.
2266
+ *
2267
+ * @internal
2268
+ */
2269
+ async function validateAccessToken(ctx, opts, token, clientId) {
2270
+ return (await resolveAccessTokenValidation(ctx, opts, token, clientId)).payload;
2271
+ }
2048
2272
  async function requireActiveAccessToken(ctx, opts, token, clientId) {
2049
2273
  const payload = await validateAccessToken(ctx, opts, token, clientId);
2050
2274
  if (payload.active) return payload;
2051
2275
  throw createInvalidAccessTokenError();
2052
2276
  }
2053
2277
  /**
2278
+ * UserInfo entry point: validates the access token, requires it active, and
2279
+ * returns the introspection payload together with the OIDC `claims.userinfo`
2280
+ * names persisted for the token. Opaque tokens carry the names on their row;
2281
+ * JWT access tokens return an empty list and resolve UserInfo claims by scope.
2282
+ */
2283
+ async function requireActiveAccessTokenWithClaims(ctx, opts, token, clientId) {
2284
+ const { payload, requestedUserInfoClaims } = await resolveAccessTokenValidation(ctx, opts, token, clientId);
2285
+ if (payload.active) return {
2286
+ payload,
2287
+ requestedUserInfoClaims
2288
+ };
2289
+ throw createInvalidAccessTokenError();
2290
+ }
2291
+ /**
2054
2292
  * Resolves pairwise sub on an introspection payload.
2055
2293
  * Applied at the presentation layer so internal validation functions
2056
2294
  * keep real user.id (needed for user lookup in /userinfo).
@@ -2116,4 +2354,4 @@ async function introspectEndpoint(ctx, opts) {
2116
2354
  }
2117
2355
  }
2118
2356
  //#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 };
2357
+ 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 };