@better-auth/oauth-provider 1.7.10 → 1.8.0-beta.1

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,56 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
- import { A as validateClientCredentials, C as resolveSessionAuthTime, D as storeToken, F as collectExtensionIdTokenClaims, H as hasUserInfoClaimExtension, I as collectExtensionUserInfoClaims, O as toAudienceClaim, P as collectExtensionAccessTokenClaims, V as getSupportedGrantTypes, _ as normalizeTimestampValue, a as extractClientCredentials, d as getStoredToken, f as isInvalidJwtError, i as destructureCredentials, j as validateClientScopes, k as toResourceList, l as getJwtPlugin, n as clientAllowsGrant, p as isPKCERequired, r as decryptStoredClientSecret, s as getClient, w as resolveSubjectIdentifier, y as parseClientMetadata, z as getExtensionGrantHandler } from "./utils-DfMNRh3J.mjs";
2
+ import { B as collectExtensionIdTokenClaims, D as resolveSessionAuthTime, F as validateClientCredentials, G as getExtensionGrantHandler, I as validateClientScopes, J as getSupportedGrantTypes, M as toAudienceClaim, N as toResourceList, O as resolveSubjectIdentifier, V as collectExtensionUserInfoClaims, X as hasUserInfoClaimExtension, a as decryptStoredClientSecret, c as extractClientCredentials, f as getJwtPlugin, h as isPKCERequired, j as storeToken, m as getStoredToken, r as clientAllowsGrant, s as destructureCredentials, u as getClient, x as parseClientMetadata, y as normalizeTimestampValue, z as collectExtensionAccessTokenClaims } from "./utils-D-CUrvd1.mjs";
3
3
  import { APIError } from "better-auth/api";
4
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";
8
- import { SignJWT, base64url, createLocalJWKSet, decodeProtectedHeader, errors, jwtVerify } from "jose";
9
8
  import { createDpopReplayStore, enforceDpopBinding, generateCodeChallenge, getConfirmationJkt, getDpopJktFromPayload, getJwks, isDpopBindingError, isDpopProofError, parseAccessTokenAuthorization, stripAccessTokenAuthorizationScheme, verifyDpopProof } from "better-auth/oauth2";
10
9
  import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
11
10
  import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
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
11
+ import { SignJWT, base64url, createLocalJWKSet, decodeProtectedHeader, errors, jwtVerify } from "jose";
54
12
  //#region src/claims-request.ts
55
13
  const claimRequestMemberSchema = z.record(z.string(), z.unknown()).nullable();
56
14
  const acrClaimRequestMemberSchema = z.looseObject({
@@ -99,6 +57,18 @@ function canSatisfyEssentialAcrRequest(value, currentAcr) {
99
57
  const valuesMatch = acrRequest.values === void 0 || acrRequest.values.includes(currentAcr);
100
58
  return valueMatches && valuesMatch;
101
59
  }
60
+ /**
61
+ * Lists the ACR values requested through `claims.id_token.acr` and the
62
+ * space-separated `acr_values` parameter, without duplicates.
63
+ */
64
+ function getRequestedAcrValues(claims, acrValues) {
65
+ const acrRequest = parseOidcClaimsRequestObject(claims)?.id_token?.acr;
66
+ return Array.from(new Set([
67
+ ...acrRequest?.value !== void 0 ? [acrRequest.value] : [],
68
+ ...acrRequest?.values ?? [],
69
+ ...acrValues?.split(" ").filter(Boolean) ?? []
70
+ ]));
71
+ }
102
72
  function filterClaimsRequestUserInfoClaims(value, allowedUserInfoClaims) {
103
73
  const claimsRequest = parseOidcClaimsRequestObject(value);
104
74
  if (!claimsRequest) return void 0;
@@ -110,6 +80,81 @@ function filterClaimsRequestUserInfoClaims(value, allowedUserInfoClaims) {
110
80
  } : Object.fromEntries(Object.entries(claimsRequest).filter(([key]) => key !== "userinfo"));
111
81
  return Object.keys(filteredClaimsRequest).length ? filteredClaimsRequest : void 0;
112
82
  }
83
+ /** The ACR values the provider may assert, advertised in discovery. */
84
+ function getAcrValuesSupported(opts) {
85
+ return opts.authenticationContext?.acrValuesSupported ?? ["0"];
86
+ }
87
+ function invalidAuthenticationContext(description) {
88
+ throw new APIError$1("INTERNAL_SERVER_ERROR", {
89
+ error: "server_error",
90
+ error_description: `authenticationContext.resolve ${description}`
91
+ });
92
+ }
93
+ /**
94
+ * Runs the configured `authenticationContext.resolve` for an approval and
95
+ * validates its result. Returns an empty context, which ID tokens report as
96
+ * `acr: "0"`, when no resolver is configured or no ID token can be issued.
97
+ * Invalid results throw an `APIError` with `server_error`.
98
+ */
99
+ async function resolveAuthenticationContext(opts, info) {
100
+ const resolve = opts.authenticationContext?.resolve;
101
+ if (!resolve || !info.scopes.includes("openid")) return {};
102
+ const { query, ...resolveInfo } = info;
103
+ const resolved = await resolve({
104
+ ...resolveInfo,
105
+ requestedAcrValues: getRequestedAcrValues(query.claims, query.acr_values)
106
+ });
107
+ if (resolved !== void 0 && (typeof resolved !== "object" || resolved === null)) invalidAuthenticationContext("returned a value that is not an object");
108
+ const { acr = "0", amr } = resolved ?? {};
109
+ if (typeof acr !== "string") invalidAuthenticationContext("returned a non-string acr");
110
+ if (!getAcrValuesSupported(opts).includes(acr)) invalidAuthenticationContext(`returned acr "${acr}", which is not in acrValuesSupported`);
111
+ if (amr === void 0) return { acr };
112
+ if (!Array.isArray(amr) || !Array.from(amr).every((value) => typeof value === "string")) invalidAuthenticationContext("returned an amr that is not a string array");
113
+ return {
114
+ acr,
115
+ amr: amr.length ? amr : void 0
116
+ };
117
+ }
118
+ const RESERVED_ID_TOKEN_CLAIMS = new Set([
119
+ "iss",
120
+ "sub",
121
+ "aud",
122
+ "exp",
123
+ "nbf",
124
+ "iat",
125
+ "jti",
126
+ "auth_time",
127
+ "nonce",
128
+ "acr",
129
+ "amr",
130
+ "azp",
131
+ "sid",
132
+ "at_hash",
133
+ "c_hash",
134
+ "s_hash"
135
+ ]);
136
+ /**
137
+ * Removes provider-owned ID token claim names from custom claims.
138
+ *
139
+ * `customIdTokenClaims` is an extension point for additional claims. It must not
140
+ * replace the issuer, subject, audience, lifetime, nonce, authorized party,
141
+ * token-binding hashes, session binding, or authentication context that the
142
+ * provider owns.
143
+ */
144
+ function stripReservedIdTokenClaims(claims) {
145
+ if (!claims) return {};
146
+ const stripped = [];
147
+ const safeClaims = Object.create(null);
148
+ for (const [key, value] of Object.entries(claims)) {
149
+ if (RESERVED_ID_TOKEN_CLAIMS.has(key)) {
150
+ stripped.push(key);
151
+ continue;
152
+ }
153
+ safeClaims[key] = value;
154
+ }
155
+ if (stripped.length > 0) logger.warn(`oauth-provider: stripped reserved id-token claim name(s): ${stripped.join(", ")}. The AS owns these claim values.`);
156
+ return safeClaims;
157
+ }
113
158
  //#endregion
114
159
  //#region src/standard-claims.ts
115
160
  function splitDisplayName(name) {
@@ -198,7 +243,8 @@ const RESERVED_ACCESS_TOKEN_CLAIMS = new Set([
198
243
  "auth_time",
199
244
  "acr",
200
245
  "amr",
201
- "cnf"
246
+ "cnf",
247
+ "reference_id"
202
248
  ]);
203
249
  /**
204
250
  * Returns a copy of `claims` with reserved AS-owned names removed. Emits a
@@ -388,6 +434,8 @@ const verificationValueSchema = z.object({
388
434
  userId: z.string(),
389
435
  referenceId: z.string().optional(),
390
436
  authTime: z.number().optional(),
437
+ acr: z.string().optional(),
438
+ amr: z.array(z.string()).optional(),
391
439
  resource: z.array(z.string()).optional()
392
440
  }).passthrough();
393
441
  /**
@@ -418,6 +466,8 @@ const clientRegistrationRequestSchema = z.object({
418
466
  post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
419
467
  backchannel_logout_uri: SafeUrlSchema.optional(),
420
468
  backchannel_logout_session_required: z.boolean().optional(),
469
+ frontchannel_logout_uri: SafeUrlSchema.optional(),
470
+ frontchannel_logout_session_required: z.boolean().optional(),
421
471
  token_endpoint_auth_method: z.string().trim().min(1).optional(),
422
472
  jwks: clientJwksSchema.optional(),
423
473
  jwks_uri: z.string().optional(),
@@ -525,40 +575,6 @@ async function assertIdentifierValid(opts, identifier) {
525
575
  */
526
576
  const userInfoResource = (baseURL) => `${baseURL}/oauth2/userinfo`;
527
577
  /**
528
- * Re-parses a request's raw `application/x-www-form-urlencoded` body to
529
- * recover every value of the repeated `resource` parameter (RFC 8707 §2).
530
- *
531
- * Workaround for better-call's form-body parser (better-call ≤1.3.5), which
532
- * collapses repeated form keys with last-write-wins. A client sending
533
- * `resource=https://a&resource=https://b` would otherwise arrive in the
534
- * handler as `{ resource: "https://b" }`, silently narrowing the issued
535
- * token's `aud` to a single resource.
536
- *
537
- * Returns the full ordered list when the body is form-encoded and contains
538
- * any `resource` entries; `undefined` otherwise. Caller MUST enable
539
- * `cloneRequest: true` on the endpoint — the body stream is read here a
540
- * second time, and that only works when better-call cloned the request
541
- * before its own parse.
542
- *
543
- * Note: the URL-encoded body path is the only one affected. The query-string
544
- * path (e.g. `/oauth2/authorize?resource=…&resource=…`) is parsed by the
545
- * router itself, which DOES array-promote duplicate keys correctly.
546
- *
547
- * @internal
548
- */
549
- async function extractRepeatedResourceFromForm(request) {
550
- if (!(request.headers.get("content-type")?.toLowerCase() ?? "").includes("application/x-www-form-urlencoded")) return;
551
- let text;
552
- try {
553
- text = await request.clone().text();
554
- } catch {
555
- return;
556
- }
557
- if (!text) return void 0;
558
- const values = new URLSearchParams(text).getAll("resource").filter((value) => value.length > 0);
559
- return values.length > 0 ? values : void 0;
560
- }
561
- /**
562
578
  * Normalizes a `resource` parameter into an array. Accepts the RFC 8707
563
579
  * forms: a single string, a string[], or undefined.
564
580
  *
@@ -1132,6 +1148,129 @@ function validateClientCredentialsScopes(scopes, grantTypes, tokenEndpointAuthMe
1132
1148
  });
1133
1149
  }
1134
1150
  //#endregion
1151
+ //#region src/token-lookup.ts
1152
+ const jwksInfrastructureErrorCodes = new Set([
1153
+ errors.JWKSTimeout.code,
1154
+ errors.JWKSInvalid.code,
1155
+ errors.JWKSMultipleMatchingKeys.code
1156
+ ]);
1157
+ /**
1158
+ * Verifies a JWT access token issued by this server. Returns `"inactive"` for
1159
+ * one that is expired or fails claim validation, so the lookup can stop: it
1160
+ * cannot be another token type. Returns `null` for anything else (an opaque
1161
+ * token, a malformed or forged JWT, a JWT that is not an access token) so the
1162
+ * lookup can continue. Key-set failures still throw.
1163
+ */
1164
+ async function verifyJwtAccessToken(ctx, opts, token) {
1165
+ if (opts.disableJwtPlugin) return null;
1166
+ if (token.split(".").length !== 3) return null;
1167
+ try {
1168
+ decodeProtectedHeader(token);
1169
+ } catch {
1170
+ return null;
1171
+ }
1172
+ const jwtPlugin = getJwtPlugin(ctx.context);
1173
+ const jwtPluginOptions = jwtPlugin.options;
1174
+ const jwks = await getJwks(token, {
1175
+ jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
1176
+ return (await jwtPlugin.endpoints.getJwks(ctx))?.response;
1177
+ },
1178
+ jwksCacheKey: jwtPlugin
1179
+ });
1180
+ let payload;
1181
+ try {
1182
+ ({payload} = await jwtVerify(token, createLocalJWKSet(jwks), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL }));
1183
+ } catch (error) {
1184
+ if (error instanceof errors.JWTExpired || error instanceof errors.JWTClaimValidationFailed) return isAccessTokenPayload(error.payload) ? "inactive" : null;
1185
+ if (error instanceof errors.JOSEError && !jwksInfrastructureErrorCodes.has(error.code)) return null;
1186
+ throw error;
1187
+ }
1188
+ return isAccessTokenPayload(payload) ? payload : null;
1189
+ }
1190
+ /**
1191
+ * The OAuth token endpoint stamps `azp` (the client) on every JWT access token.
1192
+ * The JWT plugin signs other JWTs with the same issuer and keys, such as its
1193
+ * `/token` session JWT, so one without `azp` is not an access token.
1194
+ */
1195
+ function isAccessTokenPayload(payload) {
1196
+ return typeof payload.azp === "string" && payload.azp.length > 0;
1197
+ }
1198
+ async function findOpaqueAccessToken(ctx, opts, token) {
1199
+ const prefix = opts.prefix?.opaqueAccessToken;
1200
+ if (prefix && !token.startsWith(prefix)) return null;
1201
+ return ctx.context.adapter.findOne({
1202
+ model: "oauthAccessToken",
1203
+ where: [{
1204
+ field: "token",
1205
+ value: await getStoredToken(opts.storeTokens, prefix ? token.slice(prefix.length) : token, "access_token")
1206
+ }]
1207
+ });
1208
+ }
1209
+ /**
1210
+ * Looks up a refresh token by its presented value, after removing the
1211
+ * configured prefix and applying `formatRefreshToken.decrypt`. Returns `null`
1212
+ * when the value is not a refresh token this server stored.
1213
+ */
1214
+ async function findRefreshToken(ctx, opts, token) {
1215
+ const prefix = opts.prefix?.refreshToken;
1216
+ if (prefix && !token.startsWith(prefix)) return null;
1217
+ const value = prefix ? token.slice(prefix.length) : token;
1218
+ let decoded;
1219
+ try {
1220
+ decoded = opts.formatRefreshToken?.decrypt ? await opts.formatRefreshToken.decrypt(value) : { token: value };
1221
+ } catch (error) {
1222
+ if (error instanceof APIError$1 && error.statusCode === 400 || error instanceof errors.JOSEError) return null;
1223
+ throw error;
1224
+ }
1225
+ return ctx.context.adapter.findOne({
1226
+ model: "oauthRefreshToken",
1227
+ where: [{
1228
+ field: "token",
1229
+ value: await getStoredToken(opts.storeTokens, decoded.token, "refresh_token")
1230
+ }]
1231
+ });
1232
+ }
1233
+ async function identifyAccessToken(ctx, opts, token) {
1234
+ const payload = await verifyJwtAccessToken(ctx, opts, token);
1235
+ if (payload === "inactive") return payload;
1236
+ if (payload) return {
1237
+ type: "jwt",
1238
+ payload
1239
+ };
1240
+ const record = await findOpaqueAccessToken(ctx, opts, token);
1241
+ return record ? {
1242
+ type: "opaque",
1243
+ record
1244
+ } : null;
1245
+ }
1246
+ /**
1247
+ * Identifies an access token as a JWT or an opaque token issued by this
1248
+ * server. Returns `null` when it is neither, including an expired JWT.
1249
+ */
1250
+ async function findAccessToken(ctx, opts, token) {
1251
+ const found = await identifyAccessToken(ctx, opts, token);
1252
+ return found === "inactive" ? null : found;
1253
+ }
1254
+ /**
1255
+ * Identifies a token presented to revocation or introspection. Every token
1256
+ * type is searched; `token_type_hint` only decides which one is tried first
1257
+ * (RFC 7009 §2.1, RFC 7662 §2.1). Returns `null` when no type matches. Only
1258
+ * infrastructure failures (key set, database, refresh-token decryption) throw.
1259
+ */
1260
+ async function findToken(ctx, opts, token, tokenTypeHint) {
1261
+ const asRefreshToken = async () => {
1262
+ const record = await findRefreshToken(ctx, opts, token);
1263
+ return record ? {
1264
+ type: "refresh",
1265
+ record
1266
+ } : null;
1267
+ };
1268
+ if (tokenTypeHint === "refresh_token") return await asRefreshToken() ?? findAccessToken(ctx, opts, token);
1269
+ const found = await identifyAccessToken(ctx, opts, token);
1270
+ if (found === "inactive") return null;
1271
+ return found ?? asRefreshToken();
1272
+ }
1273
+ //#endregion
1135
1274
  //#region src/userinfo.ts
1136
1275
  /**
1137
1276
  * Builds the standard OIDC claims (OIDC Core §5.1) for the UserInfo response.
@@ -1342,6 +1481,18 @@ function getOAuthProviderApi(ctx, opts, grantType) {
1342
1481
  grantType
1343
1482
  });
1344
1483
  },
1484
+ resolveConfirmation: async (params) => {
1485
+ if (params.confirmation) return params.confirmation;
1486
+ const policy = await resolveResourcePolicy(ctx, opts, {
1487
+ resource: params.resources,
1488
+ clientId: params.client.clientId,
1489
+ requestedScopes: params.scopes
1490
+ });
1491
+ return resolveDpopTokenBinding(ctx, opts, {
1492
+ client: params.client,
1493
+ grantIssuance: { dpopBoundAccessTokensRequired: policy.dpopBoundAccessTokensRequired }
1494
+ });
1495
+ },
1345
1496
  hashToken: (token, type) => storeToken(opts.storeTokens, token, type),
1346
1497
  validateAccessToken: async (token, clientId) => {
1347
1498
  const { validateAccessToken } = await Promise.resolve().then(() => introspect_exports);
@@ -1371,6 +1522,7 @@ async function createJwtAccessToken(ctx, opts, user, client, audienceClaim, scop
1371
1522
  azp: client.clientId,
1372
1523
  scope: scopes.join(" "),
1373
1524
  sid: overrides?.sid,
1525
+ reference_id: overrides?.referenceId,
1374
1526
  iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
1375
1527
  iat,
1376
1528
  exp,
@@ -1396,7 +1548,7 @@ async function computeOidcHash(token, signingAlg) {
1396
1548
  * Creates a user id token in code_authorization with scope of 'openid'
1397
1549
  * and hybrid/implicit (not yet implemented) flows
1398
1550
  */
1399
- async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken, extraClaims) {
1551
+ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken, extraClaims, authenticationContext) {
1400
1552
  const iat = Math.floor(Date.now() / 1e3);
1401
1553
  const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
1402
1554
  const resolvedSub = await resolveSubjectIdentifier(user.id, client, opts);
@@ -1410,11 +1562,12 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
1410
1562
  const resolvedKey = !opts.disableJwtPlugin && !jwtPluginOptions?.jwt?.sign ? await resolveSigningKey(ctx, jwtPluginOptions) : void 0;
1411
1563
  const signingAlg = opts.disableJwtPlugin ? "HS256" : resolvedKey?.alg ?? jwtPluginOptions?.jwks?.keyPairConfig?.alg;
1412
1564
  const atHash = accessToken ? await computeOidcHash(accessToken, signingAlg) : void 0;
1413
- const emitSid = Boolean(client.enableEndSession || client.backchannelLogoutUri);
1565
+ const emitSid = Boolean(client.enableEndSession || client.backchannelLogoutUri || client.frontchannelLogoutUri);
1414
1566
  const payload = {
1415
1567
  ...ID_TOKEN_SCOPE_CLAIM_GUARDS,
1416
1568
  auth_time: authTimeSec,
1417
- acr: "0",
1569
+ acr: authenticationContext?.acr ?? "0",
1570
+ amr: authenticationContext?.amr,
1418
1571
  ...customClaims,
1419
1572
  at_hash: atHash,
1420
1573
  iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
@@ -1446,20 +1599,7 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
1446
1599
  * Encodes a refresh token for a client
1447
1600
  */
1448
1601
  async function encodeRefreshToken(opts, token, sessionId) {
1449
- return (opts.prefix?.refreshToken ?? "") + (opts.formatRefreshToken?.encrypt ? opts.formatRefreshToken.encrypt(token, sessionId) : token);
1450
- }
1451
- /**
1452
- * Decodes a refresh token for a client
1453
- *
1454
- * @internal
1455
- */
1456
- async function decodeRefreshToken(opts, token) {
1457
- if (opts.prefix?.refreshToken) if (token.startsWith(opts.prefix.refreshToken)) token = token.replace(opts.prefix.refreshToken, "");
1458
- else throw new APIError("BAD_REQUEST", {
1459
- error_description: "refresh token not found",
1460
- error: "invalid_token"
1461
- });
1462
- return opts.formatRefreshToken?.decrypt ? opts.formatRefreshToken?.decrypt(token) : { token };
1602
+ return (opts.prefix?.refreshToken ?? "") + (opts.formatRefreshToken?.encrypt ? await opts.formatRefreshToken.encrypt(token, sessionId) : token);
1463
1603
  }
1464
1604
  async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, authorizationCodeId, refreshId, confirmation, requestedUserInfoClaims) {
1465
1605
  const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
@@ -1549,7 +1689,7 @@ async function revokeTokensIssuedForAuthorizationCode(ctx, authorizationCodeId)
1549
1689
  await deleteIssuedTokens("oauthAccessToken");
1550
1690
  await deleteIssuedTokens("oauthRefreshToken");
1551
1691
  }
1552
- async function createRefreshToken(ctx, opts, user, referenceId, authorizationCodeId, client, scopes, payload, originalRefresh, authTime, resources, confirmation, requestedUserInfoClaims) {
1692
+ async function createRefreshToken(ctx, opts, user, referenceId, authorizationCodeId, client, scopes, payload, originalRefresh, authTime, resources, confirmation, requestedUserInfoClaims, authenticationContext) {
1553
1693
  const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
1554
1694
  const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
1555
1695
  const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
@@ -1562,6 +1702,8 @@ async function createRefreshToken(ctx, opts, user, referenceId, authorizationCod
1562
1702
  referenceId,
1563
1703
  authorizationCodeId,
1564
1704
  authTime,
1705
+ acr: authenticationContext?.acr,
1706
+ amr: authenticationContext?.amr,
1565
1707
  confirmation,
1566
1708
  requestedUserInfoClaims: requestedUserInfoClaims?.length ? requestedUserInfoClaims : void 0,
1567
1709
  scopes,
@@ -1784,9 +1926,18 @@ function replayRefreshTokenRotationResponse(ctx, response) {
1784
1926
  });
1785
1927
  }
1786
1928
  async function createUserTokens(ctx, opts, params) {
1787
- const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, verificationValue } = params;
1929
+ const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, acr, amr, verificationValue } = params;
1930
+ const authenticationContext = {
1931
+ acr,
1932
+ amr
1933
+ };
1788
1934
  const iat = Math.floor(Date.now() / 1e3);
1789
- const defaultExp = iat + (user ? opts.accessTokenExpiresIn ?? 3600 : opts.m2mAccessTokenExpiresIn ?? 3600);
1935
+ const baseExpiry = user ? opts.accessTokenExpiresIn ?? 3600 : opts.m2mAccessTokenExpiresIn ?? 3600;
1936
+ const defaultExp = Math.min(iat + baseExpiry, params.maxExpiresAt ? Math.floor(params.maxExpiresAt.getTime() / 1e3) : Number.POSITIVE_INFINITY);
1937
+ if (defaultExp <= iat) throw new APIError("BAD_REQUEST", {
1938
+ error: "invalid_request",
1939
+ error_description: "the token this grant derives from has expired"
1940
+ });
1790
1941
  const scopeExp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
1791
1942
  return prev < curr ? prev : curr;
1792
1943
  }, defaultExp) : defaultExp;
@@ -1841,7 +1992,7 @@ async function createUserTokens(ctx, opts, params) {
1841
1992
  iat,
1842
1993
  exp: refreshTokenExp,
1843
1994
  sid: sessionId
1844
- }, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0;
1995
+ }, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims, authenticationContext) : void 0;
1845
1996
  const accessTokenClaims = isJwtAccessToken ? await resolveAccessTokenClaims({
1846
1997
  ctx,
1847
1998
  opts,
@@ -1860,6 +2011,7 @@ async function createUserTokens(ctx, opts, params) {
1860
2011
  iat,
1861
2012
  exp,
1862
2013
  sid: sessionId,
2014
+ referenceId,
1863
2015
  signingAlgorithm: grantIssuance.signingAlgorithm,
1864
2016
  signingKeyId: grantIssuance.signingKeyId,
1865
2017
  accessTokenClaims,
@@ -1872,8 +2024,8 @@ async function createUserTokens(ctx, opts, params) {
1872
2024
  iat,
1873
2025
  exp: refreshTokenExp,
1874
2026
  sid: sessionId
1875
- }, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0]);
1876
- const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, effectiveScopes, nonce, sessionId, authTime, accessToken, additionalIdTokenClaims) : void 0;
2027
+ }, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims, authenticationContext) : void 0]);
2028
+ const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, effectiveScopes, nonce, sessionId, authTime, accessToken, additionalIdTokenClaims, authenticationContext) : void 0;
1877
2029
  const responseBody = {
1878
2030
  ...customFields,
1879
2031
  ...params.tokenResponse ?? {},
@@ -2051,6 +2203,8 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
2051
2203
  sessionId: session.id,
2052
2204
  nonce: verificationValue.query?.nonce,
2053
2205
  authTime,
2206
+ acr: verificationValue.acr,
2207
+ amr: verificationValue.amr,
2054
2208
  verificationValue,
2055
2209
  authorizationCodeId,
2056
2210
  requestedUserInfoClaims,
@@ -2123,14 +2277,7 @@ async function handleRefreshTokenGrant(ctx, opts) {
2123
2277
  * `invalid_client`, so resolve it before the refresh token lookup can
2124
2278
  * report `invalid_grant`). */
2125
2279
  const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, "refresh_token", authMethod);
2126
- const decodedRefresh = await decodeRefreshToken(opts, refresh_token);
2127
- const refreshToken = await ctx.context.adapter.findOne({
2128
- model: "oauthRefreshToken",
2129
- where: [{
2130
- field: "token",
2131
- value: await getStoredToken(opts.storeTokens, decodedRefresh.token, "refresh_token")
2132
- }]
2133
- });
2280
+ const refreshToken = await findRefreshToken(ctx, opts, refresh_token);
2134
2281
  if (!refreshToken) throw new APIError("BAD_REQUEST", {
2135
2282
  error_description: "session not found",
2136
2283
  error: "invalid_grant"
@@ -2194,6 +2341,8 @@ async function handleRefreshTokenGrant(ctx, opts) {
2194
2341
  refreshToken,
2195
2342
  resources: resources ?? refreshToken.resources,
2196
2343
  authTime,
2344
+ acr: refreshToken.acr ?? void 0,
2345
+ amr: refreshToken.amr ?? void 0,
2197
2346
  requestedUserInfoClaims: refreshToken.requestedUserInfoClaims
2198
2347
  });
2199
2348
  }
@@ -2214,11 +2363,11 @@ function inactiveAccessToken() {
2214
2363
  };
2215
2364
  }
2216
2365
  /**
2217
- * IMPORTANT NOTES:
2218
- * Introspection follows RFC7662
2366
+ * Introspection follows RFC 7662
2219
2367
  * https://datatracker.ietf.org/doc/html/rfc7662
2220
- * - APIError: Continue catches (returnable to client)
2221
- * - Error: Should immediately stop catches (internal error)
2368
+ *
2369
+ * A token this server cannot find, or one the caller may not see, resolves as
2370
+ * `{ active: false }` (§2.2, §4). Only protocol errors surface.
2222
2371
  */
2223
2372
  /**
2224
2373
  * Resource identifiers in a token's audience, excluding the implicit
@@ -2244,38 +2393,14 @@ async function isIntrospectionAuthorized(ctx, opts, params) {
2244
2393
  return isClientLinkedToAnyResource(ctx, opts, introspectingClientId, audienceResources);
2245
2394
  }
2246
2395
  /**
2247
- * Validates a JWT access token against the configured JWKs.
2396
+ * Validates a JWT access token whose signature and issuer are verified.
2248
2397
  *
2249
2398
  * @returns RFC7662 introspection format
2250
2399
  */
2251
- async function validateJwtAccessToken(ctx, opts, token, clientId) {
2252
- if (opts.disableJwtPlugin) throw new APIError$1("BAD_REQUEST", {
2253
- error_description: "JWT access tokens are disabled",
2254
- error: "invalid_request"
2255
- });
2256
- const jwtPlugin = getJwtPlugin(ctx.context);
2257
- const jwtPluginOptions = jwtPlugin.options;
2258
- const userInfoAud = `${ctx.context.baseURL ?? ""}/oauth2/userinfo`;
2259
- const expectedIssuer = jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL;
2260
- let jwtPayload;
2261
- try {
2262
- jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
2263
- jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
2264
- return (await jwtPlugin.endpoints.getJwks(ctx))?.response;
2265
- },
2266
- jwksCacheKey: jwtPlugin
2267
- })), { issuer: expectedIssuer })).payload;
2268
- } catch (error) {
2269
- if (error instanceof errors.JWTExpired || error instanceof errors.JWTInvalid) return { active: false };
2270
- if (error instanceof TypeError || isInvalidJwtError(error)) throw new APIError$1("BAD_REQUEST", {
2271
- error_description: "invalid JWT signature",
2272
- error: "invalid_request"
2273
- });
2274
- throw error;
2275
- }
2400
+ async function validateJwtAccessToken(ctx, opts, jwtPayload, clientId) {
2401
+ const userInfoAud = userInfoResource(ctx.context.baseURL ?? "");
2276
2402
  const rawAud = jwtPayload.aud;
2277
2403
  if (!await isAudienceClaimAllowed(ctx, opts, rawAud, [userInfoAud])) return { active: false };
2278
- if (!jwtPayload.azp) return { active: false };
2279
2404
  const client = await getClient(ctx, opts, jwtPayload.azp);
2280
2405
  if (!client || client?.disabled) return { active: false };
2281
2406
  if (!await isIntrospectionAuthorized(ctx, opts, {
@@ -2300,28 +2425,11 @@ async function validateJwtAccessToken(ctx, opts, token, clientId) {
2300
2425
  return jwtPayload;
2301
2426
  }
2302
2427
  /**
2303
- * Searches for an opaque access token in the database and validates it
2428
+ * Validates an opaque access token record.
2304
2429
  *
2305
2430
  * @returns RFC7662 introspection format
2306
2431
  */
2307
- async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
2308
- let tokenValue = token;
2309
- if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
2310
- else throw new APIError$1("BAD_REQUEST", {
2311
- error_description: "opaque access token not found",
2312
- error: "invalid_request"
2313
- });
2314
- const accessToken = await ctx.context.adapter.findOne({
2315
- model: "oauthAccessToken",
2316
- where: [{
2317
- field: "token",
2318
- value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
2319
- }]
2320
- });
2321
- if (!accessToken) throw new APIError$1("BAD_REQUEST", {
2322
- error_description: "opaque access token not found",
2323
- error: "invalid_token"
2324
- });
2432
+ async function validateOpaqueAccessToken(ctx, opts, accessToken, clientId) {
2325
2433
  if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return inactiveAccessToken();
2326
2434
  if (accessToken.revoked) return inactiveAccessToken();
2327
2435
  const resources = Array.isArray(accessToken.resources) ? accessToken.resources : void 0;
@@ -2380,6 +2488,7 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
2380
2488
  azp: accessToken.clientId,
2381
2489
  sub: user?.id,
2382
2490
  sid: sessionId,
2491
+ reference_id: accessToken.referenceId ?? void 0,
2383
2492
  exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
2384
2493
  iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
2385
2494
  scope: accessToken.scopes?.join(" "),
@@ -2390,22 +2499,11 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
2390
2499
  };
2391
2500
  }
2392
2501
  /**
2393
- * Validates a refresh token in the session store.
2502
+ * Validates a refresh token record.
2394
2503
  *
2395
2504
  * @returns payload in RFC7662 introspection format
2396
2505
  */
2397
- async function validateRefreshToken(ctx, opts, token, clientId) {
2398
- const refreshToken = await ctx.context.adapter.findOne({
2399
- model: "oauthRefreshToken",
2400
- where: [{
2401
- field: "token",
2402
- value: await getStoredToken(opts.storeTokens, token, "refresh_token")
2403
- }]
2404
- });
2405
- if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
2406
- error_description: "token not found",
2407
- error: "invalid_token"
2408
- });
2506
+ async function validateRefreshToken(ctx, opts, refreshToken, clientId) {
2409
2507
  if (!refreshToken.clientId || refreshToken.clientId !== clientId) return { active: false };
2410
2508
  if (!refreshToken.expiresAt || refreshToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
2411
2509
  if (refreshToken.revoked) return { active: false };
@@ -2428,6 +2526,7 @@ async function validateRefreshToken(ctx, opts, token, clientId) {
2428
2526
  iss: ((opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options)?.jwt?.issuer ?? ctx.context.baseURL,
2429
2527
  sub: user?.id,
2430
2528
  sid: sessionId,
2529
+ reference_id: refreshToken.referenceId ?? void 0,
2431
2530
  exp: Math.floor(new Date(refreshToken.expiresAt).getTime() / 1e3),
2432
2531
  iat: Math.floor(new Date(refreshToken.createdAt).getTime() / 1e3),
2433
2532
  scope: refreshToken.scopes?.join(" "),
@@ -2441,32 +2540,23 @@ function createInvalidAccessTokenError() {
2441
2540
  error: "invalid_token"
2442
2541
  }, { "WWW-Authenticate": INVALID_ACCESS_TOKEN_WWW_AUTHENTICATE });
2443
2542
  }
2444
- function isInactiveTokenError(error) {
2445
- return error.status === "BAD_REQUEST" || error.body?.error === "invalid_token";
2543
+ async function validateFoundAccessToken(ctx, opts, found, clientId) {
2544
+ if (found.type === "opaque") return validateOpaqueAccessToken(ctx, opts, found.record, clientId);
2545
+ return {
2546
+ payload: await validateJwtAccessToken(ctx, opts, found.payload, clientId),
2547
+ requestedUserInfoClaims: []
2548
+ };
2446
2549
  }
2447
2550
  /**
2448
- * We don't know the access token format so we try to validate it as a JWT
2449
- * first, then as an opaque token. Returns the RFC 7662 introspection payload
2450
- * alongside the per-issuance UserInfo claims the opaque row persisted (empty for
2451
- * a JWT access token, which resolves UserInfo claims by scope).
2551
+ * Validates an access token (JWT or opaque). Returns the RFC 7662
2552
+ * introspection payload, inactive for an unknown token, alongside the
2553
+ * per-issuance UserInfo claims the opaque row persisted (empty for a JWT access
2554
+ * token, which resolves UserInfo claims by scope).
2452
2555
  */
2453
2556
  async function resolveAccessTokenValidation(ctx, opts, token, clientId) {
2454
- try {
2455
- return {
2456
- payload: await validateJwtAccessToken(ctx, opts, token, clientId),
2457
- requestedUserInfoClaims: []
2458
- };
2459
- } catch (err) {
2460
- if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
2461
- else throw new Error(err);
2462
- }
2463
- try {
2464
- return await validateOpaqueAccessToken(ctx, opts, token, clientId);
2465
- } catch (err) {
2466
- if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
2467
- else throw new Error("Unknown error validating access token");
2468
- }
2469
- throw createInvalidAccessTokenError();
2557
+ const found = await findAccessToken(ctx, opts, token);
2558
+ if (!found) return inactiveAccessToken();
2559
+ return validateFoundAccessToken(ctx, opts, found, clientId);
2470
2560
  }
2471
2561
  /**
2472
2562
  * Validates an access token (JWT or opaque) and returns its RFC 7662
@@ -2515,7 +2605,6 @@ async function resolveIntrospectionSub(ctx, opts, payload, introspectingClient)
2515
2605
  }
2516
2606
  async function introspectEndpoint(ctx, opts) {
2517
2607
  let { token, token_type_hint } = ctx.body;
2518
- if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
2519
2608
  const { clientId: client_id, clientSecret: client_secret, preVerified, authMethod } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
2520
2609
  if (!client_id || !client_secret && !preVerified) throw new APIError$1("UNAUTHORIZED", {
2521
2610
  error_description: "missing required credentials",
@@ -2528,38 +2617,16 @@ async function introspectEndpoint(ctx, opts) {
2528
2617
  });
2529
2618
  const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, void 0, authMethod);
2530
2619
  try {
2531
- if (token_type_hint === void 0 || token_type_hint === "access_token") try {
2532
- return resolveIntrospectionSub(ctx, opts, await validateAccessToken(ctx, opts, token, client.clientId), client);
2533
- } catch (error) {
2534
- if (error instanceof APIError$1) {
2535
- if (token_type_hint === "access_token") throw error;
2536
- } else if (error instanceof Error) throw error;
2537
- else throw new Error(error);
2538
- }
2539
- if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
2540
- return resolveIntrospectionSub(ctx, opts, await validateRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId), client);
2541
- } catch (error) {
2542
- if (error instanceof APIError$1) {
2543
- if (token_type_hint === "refresh_token") throw error;
2544
- } else if (error instanceof Error) throw error;
2545
- else throw new Error(error);
2546
- }
2547
- throw new APIError$1("BAD_REQUEST", {
2548
- error_description: "token not found",
2549
- error: "invalid_request"
2550
- });
2620
+ const found = await findToken(ctx, opts, token, token_type_hint);
2621
+ if (!found) return { active: false };
2622
+ if (found.type === "refresh") return resolveIntrospectionSub(ctx, opts, await validateRefreshToken(ctx, opts, found.record, client.clientId), client);
2623
+ const { payload } = await validateFoundAccessToken(ctx, opts, found, client.clientId);
2624
+ return resolveIntrospectionSub(ctx, opts, payload, client);
2551
2625
  } catch (error) {
2552
- if (error instanceof APIError$1) {
2553
- if (isInactiveTokenError(error)) return { active: false };
2554
- throw error;
2555
- } else if (error instanceof Error) {
2556
- logger.error("Introspection error:", error.message, error.stack);
2557
- throw new APIError$1("INTERNAL_SERVER_ERROR");
2558
- } else {
2559
- logger.error("Introspection error:", error);
2560
- throw new APIError$1("INTERNAL_SERVER_ERROR");
2561
- }
2626
+ if (error instanceof APIError$1) throw error;
2627
+ logger.error("Introspection error:", error);
2628
+ throw new APIError$1("INTERNAL_SERVER_ERROR");
2562
2629
  }
2563
2630
  }
2564
2631
  //#endregion
2565
- export { canSatisfyEssentialAcrRequest as A, authorizationQuerySchema as C, STANDARD_CLAIMS as D, oauthClientMetadataSchema as E, filterClaimsRequestUserInfoClaims as M, getRequestedUserInfoClaims as N, STANDARD_CLAIM_NAMES as O, isValidOidcClaimsRequest as P, SafeUrlSchema as S, clientRegistrationRequestSchema as T, logEnforcePerClientResourcesResolution as _, invalidateRefreshFamily as a, seedResourcesOnce as b, normalizeClientCredentialsScopes as c, JWS_ALGORITHMS as d, assertIdentifierValid as f, isAudienceClaimAllowed as g, invalidateResourceCache as h, getOAuthProviderApi as i, claimsRequestParameterSchema as j, getSupportedClaims as k, validateClientCredentialsScopes as l, getResource as m, introspect_exports as n, tokenEndpoint as o, extractRepeatedResourceFromForm as p, decodeRefreshToken as r, userInfoEndpoint as s, introspectEndpoint as t, getEndpointUrl as u, logStartupSeedFailure as v, clientJwksSchema as w, ResourceUriSchema as x, resolveResourcePolicy as y };
2632
+ export { getAcrValuesSupported as A, authorizationQuerySchema as C, STANDARD_CLAIMS as D, oauthClientMetadataSchema as E, getRequestedUserInfoClaims as F, isValidOidcClaimsRequest as I, canSatisfyEssentialAcrRequest as M, claimsRequestParameterSchema as N, STANDARD_CLAIM_NAMES as O, filterClaimsRequestUserInfoClaims as P, SafeUrlSchema as S, clientRegistrationRequestSchema as T, logStartupSeedFailure as _, tokenEndpoint as a, userInfoResource as b, normalizeClientCredentialsScopes as c, JWS_ALGORITHMS as d, assertIdentifierValid as f, logEnforcePerClientResourcesResolution as g, isAudienceClaimAllowed as h, invalidateRefreshFamily as i, resolveAuthenticationContext as j, getSupportedClaims as k, validateClientCredentialsScopes as l, invalidateResourceCache as m, introspect_exports as n, userInfoEndpoint as o, getResource as p, getOAuthProviderApi as r, findToken as s, introspectEndpoint as t, getEndpointUrl as u, resolveResourcePolicy as v, clientJwksSchema as w, ResourceUriSchema as x, seedResourcesOnce as y };