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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,25 +1,26 @@
1
- import { n as isPrivateHostname } from "./client-assertion-DLMKVgoj.mjs";
2
- import { n as mcpHandler } from "./mcp-CYnz-MXn.mjs";
3
- import { C as toAudienceClaim, D as verifyOAuthQueryParams, E as validateClientCredentials, S as storeToken, T as toResourceList, _ as resolveSessionAuthTime, a as getClient, b as signedQueryIssuedAtParam, c as getSignedQueryIssuedAt, d as mergeDiscoveryMetadata, f as normalizeTimestampValue, g as removePromptFromQuery, h as postLoginClearedParam, i as extractClientCredentials, l as getStoredToken, m as parsePrompt, n as decryptStoredClientSecret, o as getJwtPlugin, p as parseClientMetadata, r as destructureCredentials, t as checkResource, u as isPKCERequired, v as resolveSubjectIdentifier, w as toClientDiscoveryArray, x as storeClientSecret, y as searchParamsToQuery } from "./utils-DKBWQ8fe.mjs";
4
- import { t as PACKAGE_VERSION } from "./version-nFnRm-a3.mjs";
5
- import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
6
- import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
7
- import { APIError as APIError$1 } from "better-call";
8
- import { PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
1
+ import { D as applyOAuthProviderMetadataExtensions, E as verifyOAuthQueryParams, F as getSupportedGrantTypes, L as isExtensionTokenEndpointAuthMethod, M as getClientDiscoveries, P as getSupportedAuthMethods, R as validateOAuthProviderExtensions, S as storeToken, T as validateClientCredentials, _ as removePromptFromQuery, a as getClient, b as searchParamsToQuery, c as getStoredToken, d as mergeDiscoveryMetadata, g as removeMaxAgeFromQuery, h as parsePrompt, i as extractClientCredentials, j as extendOAuthProvider, l as isPKCERequired, m as parseClientMetadata, n as decryptStoredClientSecret, o as getJwtPlugin, p as parseBearerToken, r as destructureCredentials, t as clientAllowsGrant, u as isSessionFreshForSignedQuery, w as toResourceList, x as storeClientSecret, y as resolveSubjectIdentifier } from "./utils-Baq6atYN.mjs";
2
+ import { a as setSignedOAuthQueryParameterNames, i as postLoginClearedParam, n as canonicalizeOAuthQueryParams, o as signedQueryIssuedAtParam, r as getSignedQueryIssuedAt } from "./signed-query-CFv2jNMT.mjs";
3
+ import { _ as invalidateResourceCache, a as invalidateRefreshFamily, b as resolveResourcePolicy, c as ResourceUriSchema, d as clientRegistrationRequestSchema, f as JWS_ALGORITHMS, g as getResource, h as extractRepeatedResourceFromForm, i as getOAuthProviderApi, l as SafeUrlSchema, m as buildClientResourceLinkId, o as tokenEndpoint, p as assertIdentifierValid, r as decodeRefreshToken, s as userInfoEndpoint, t as introspectEndpoint, u as authorizationQuerySchema, v as isAudienceClaimAllowed, x as seedResources, y as logEnforcePerClientResourcesResolution } from "./introspect-BXqKFUQZ.mjs";
4
+ import { n as consumeClientAssertion, r as isPrivateHostname } from "./client-assertion-CctbJywV.mjs";
5
+ import { t as PACKAGE_VERSION } from "./version-CUu3vBtU.mjs";
6
+ import { t as raiseResourceServerChallenge } from "./resource-challenge-B-cqv4ur.mjs";
9
7
  import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
10
8
  import { isLoopbackHost, isLoopbackIP } from "@better-auth/core/utils/host";
9
+ import { APIError, NO_STORE_HEADERS, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, dispatchAuthEndpoint, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
11
10
  import { generateRandomString, makeSignature } from "better-auth/crypto";
12
- import { defineRequestState } from "@better-auth/core/context";
11
+ import { APIError as APIError$1 } from "better-call";
12
+ import { defineRequestState, runWithTransaction } from "@better-auth/core/context";
13
13
  import { logger } from "@better-auth/core/env";
14
14
  import { BetterAuthError } from "@better-auth/core/error";
15
15
  import { parseSetCookieHeader } from "better-auth/cookies";
16
16
  import { mergeSchema } from "better-auth/db";
17
17
  import * as z from "zod";
18
+ import { DPOP_SIGNING_ALGORITHMS, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
19
+ import { getJwks, stripAccessTokenAuthorizationScheme } from "better-auth/oauth2";
20
+ import { compactVerify, createLocalJWKSet, decodeJwt, jwtVerify } from "jose";
18
21
  import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
19
- import { SignJWT, base64url, compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader } from "jose";
20
- import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
21
22
  //#region src/consent.ts
22
- async function consentEndpoint(ctx, opts) {
23
+ async function consentEndpoint(ctx, opts, authorize) {
23
24
  const oauthRequest = await oAuthState.get();
24
25
  const _query = oauthRequest?.query;
25
26
  if (!_query) throw new APIError("BAD_REQUEST", {
@@ -46,15 +47,11 @@ async function consentEndpoint(ctx, opts) {
46
47
  };
47
48
  const session = await getSessionFromCtx(ctx);
48
49
  const hasLoginPrompt = parsePrompt(query.get("prompt") ?? "").has("login");
49
- const hasSatisfiedLoginPrompt = hasLoginPrompt && sessionSatisfiesLoginPrompt(session?.session.createdAt, oauthRequest?.signedQueryIssuedAt);
50
+ const hasSatisfiedLoginPrompt = hasLoginPrompt && isSessionFreshForSignedQuery(session?.session.createdAt, oauthRequest?.signedQueryIssuedAt);
50
51
  if (hasLoginPrompt && !hasSatisfiedLoginPrompt) {
51
52
  ctx?.headers?.set("accept", "application/json");
52
53
  ctx.query = searchParamsToQuery(query);
53
- const { url } = await authorizeEndpoint(ctx, opts);
54
- return {
55
- redirect: true,
56
- url
57
- };
54
+ return await authorize(ctx);
58
55
  }
59
56
  const referenceId = await opts.postLogin?.consentReferenceId?.({
60
57
  user: session?.user,
@@ -110,32 +107,25 @@ async function consentEndpoint(ctx, opts) {
110
107
  if (requestedScopes) query.set("scope", consent.scopes.join(" "));
111
108
  ctx?.headers?.set("accept", "application/json");
112
109
  let authorizationQuery = removePromptFromQuery(query, "consent");
113
- if (hasSatisfiedLoginPrompt) authorizationQuery = removePromptFromQuery(authorizationQuery, "login");
110
+ if (hasSatisfiedLoginPrompt) {
111
+ authorizationQuery = removePromptFromQuery(authorizationQuery, "login");
112
+ authorizationQuery = removeMaxAgeFromQuery(authorizationQuery);
113
+ }
114
114
  ctx.query = searchParamsToQuery(authorizationQuery);
115
- const { url } = await authorizeEndpoint(ctx, opts, { postLogin: oauthRequest?.postLoginClearedForSession !== void 0 && oauthRequest.postLoginClearedForSession === session?.session.id });
116
- return {
117
- redirect: true,
118
- url
119
- };
120
- }
121
- function sessionSatisfiesLoginPrompt(sessionCreatedAt, signedQueryIssuedAt) {
122
- if (!signedQueryIssuedAt) return false;
123
- const normalized = normalizeTimestampValue(sessionCreatedAt);
124
- if (!normalized) return false;
125
- return normalized.getTime() >= signedQueryIssuedAt.getTime();
115
+ return await authorize(ctx, { postLogin: oauthRequest?.postLoginClearedForSession !== void 0 && oauthRequest.postLoginClearedForSession === session?.session.id });
126
116
  }
127
117
  //#endregion
128
118
  //#region src/continue.ts
129
- async function continueEndpoint(ctx, opts) {
130
- if (ctx.body.selected === true) return await selected(ctx, opts);
131
- else if (ctx.body.created === true) return await created(ctx, opts);
132
- else if (ctx.body.postLogin === true) return await postLogin(ctx, opts);
119
+ async function continueEndpoint(ctx, authorize) {
120
+ if (ctx.body.selected === true) return await selected(ctx, authorize);
121
+ else if (ctx.body.created === true) return await created(ctx, authorize);
122
+ else if (ctx.body.postLogin === true) return await postLogin(ctx, authorize);
133
123
  else throw new APIError("BAD_REQUEST", {
134
124
  error_description: "Missing parameters",
135
125
  error: "invalid_request"
136
126
  });
137
127
  }
138
- async function selected(ctx, opts) {
128
+ async function selected(ctx, authorize) {
139
129
  const _query = (await oAuthState.get())?.query;
140
130
  if (!_query) throw new APIError("BAD_REQUEST", {
141
131
  error_description: "missing oauth query",
@@ -143,13 +133,9 @@ async function selected(ctx, opts) {
143
133
  });
144
134
  ctx.headers?.set("accept", "application/json");
145
135
  ctx.query = searchParamsToQuery(removePromptFromQuery(new URLSearchParams(_query), "select_account"));
146
- const { url } = await authorizeEndpoint(ctx, opts);
147
- return {
148
- redirect: true,
149
- url
150
- };
136
+ return await authorize(ctx);
151
137
  }
152
- async function created(ctx, opts) {
138
+ async function created(ctx, authorize) {
153
139
  const _query = (await oAuthState.get())?.query;
154
140
  if (!_query) throw new APIError("BAD_REQUEST", {
155
141
  error_description: "missing oauth query",
@@ -158,14 +144,11 @@ async function created(ctx, opts) {
158
144
  const query = new URLSearchParams(_query);
159
145
  ctx.headers?.set("accept", "application/json");
160
146
  ctx.query = searchParamsToQuery(removePromptFromQuery(query, "create"));
161
- const { url } = await authorizeEndpoint(ctx, opts);
162
- return {
163
- redirect: true,
164
- url
165
- };
147
+ return await authorize(ctx);
166
148
  }
167
- async function postLogin(ctx, opts) {
168
- const _query = (await oAuthState.get())?.query;
149
+ async function postLogin(ctx, authorize) {
150
+ const state = await oAuthState.get();
151
+ const _query = state?.query;
169
152
  if (!_query) throw new APIError("BAD_REQUEST", {
170
153
  error_description: "missing oauth query",
171
154
  error: "invalid_request"
@@ -173,981 +156,178 @@ async function postLogin(ctx, opts) {
173
156
  const query = new URLSearchParams(_query);
174
157
  ctx.headers?.set("accept", "application/json");
175
158
  ctx.query = searchParamsToQuery(query);
176
- const { url } = await authorizeEndpoint(ctx, opts, { postLogin: true });
177
- return {
178
- redirect: true,
179
- url
180
- };
159
+ const session = await getSessionFromCtx(ctx);
160
+ return await authorize(ctx, { postLogin: state?.postLoginClearedForSession !== void 0 && state.postLoginClearedForSession === session?.session.id });
181
161
  }
182
162
  //#endregion
183
- //#region src/types/zod.ts
184
- /**
185
- * Runtime schema for OAuthAuthorizationQuery.
186
- * Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
187
- */
188
- const oauthAuthorizationQuerySchema = z.object({
189
- response_type: z.literal("code").optional(),
190
- request_uri: z.string().optional(),
191
- redirect_uri: z.string(),
192
- scope: z.string().optional(),
193
- state: z.string().optional(),
194
- client_id: z.string(),
195
- prompt: z.string().optional(),
196
- display: z.string().optional(),
197
- ui_locales: z.string().optional(),
198
- max_age: z.coerce.number().optional(),
199
- acr_values: z.string().optional(),
200
- login_hint: z.string().optional(),
201
- id_token_hint: z.string().optional(),
202
- code_challenge: z.string().optional(),
203
- code_challenge_method: z.literal("S256").optional(),
204
- nonce: z.string().optional(),
205
- resource: z.union([z.string(), z.array(z.string())]).optional()
206
- }).passthrough();
207
- /**
208
- * Runtime schema for the authorization code verification value.
209
- * Validates structure on deserialization from the JSON blob stored in the DB.
210
- * Uses passthrough so future fields (e.g. from authorization challenge) don't break parsing.
211
- */
212
- const verificationValueSchema = z.object({
213
- type: z.literal("authorization_code"),
214
- query: oauthAuthorizationQuerySchema,
215
- sessionId: z.string(),
216
- userId: z.string(),
217
- referenceId: z.string().optional(),
218
- authTime: z.number().optional(),
219
- resource: z.array(z.string()).optional()
220
- }).passthrough();
221
- const DANGEROUS_SCHEMES = [
222
- "javascript:",
223
- "data:",
224
- "vbscript:"
225
- ];
226
- /**
227
- * Validates an RFC 8707 resource indicator. The value must be an absolute URI
228
- * with no fragment (RFC 8707 §2). Unlike a redirect URI it is not restricted to
229
- * HTTPS, because a resource server identifier may use any absolute URI scheme;
230
- * the configured `validAudiences` allowlist is the authoritative control over
231
- * which resources a token may target.
232
- */
233
- const ResourceUriSchema = z.string().superRefine((val, ctx) => {
234
- if (!URL.canParse(val)) {
235
- ctx.addIssue({
236
- code: "custom",
237
- message: "resource must be an absolute URI",
238
- fatal: true
239
- });
240
- return z.NEVER;
241
- }
242
- if (val.includes("#")) {
243
- ctx.addIssue({
244
- code: "custom",
245
- message: "resource must not contain a fragment"
246
- });
247
- return;
248
- }
249
- if (DANGEROUS_SCHEMES.includes(new URL(val).protocol)) ctx.addIssue({
250
- code: "custom",
251
- message: "resource cannot use javascript:, data:, or vbscript: scheme"
252
- });
253
- });
254
- //#endregion
255
- //#region src/userinfo.ts
163
+ //#region src/logout.ts
164
+ const BACKCHANNEL_LOGOUT_EVENT_URI = "http://schemas.openid.net/event/backchannel-logout";
165
+ const LOGOUT_TOKEN_JWT_TYP = "logout+jwt";
166
+ const LOGOUT_TOKEN_LIFETIME_SECONDS = 120;
167
+ const BACKCHANNEL_DISPATCH_TIMEOUT_MS = 5e3;
256
168
  /**
257
- * Provides shared /userinfo and id_token claims functionality
169
+ * Signs a Back-Channel Logout Token per OIDC Back-Channel Logout 1.0 §2.4.
258
170
  *
259
- * @see https://openid.net/specs/openid-connect-core-1_0.html#NormalClaims
260
- */
261
- function userNormalClaims(user, scopes) {
262
- const name = user.name.split(" ").filter((v) => v !== "");
263
- const profile = {
264
- name: user.name ?? void 0,
265
- picture: user.image ?? void 0,
266
- given_name: name.length > 1 ? name.slice(0, -1).join(" ") : void 0,
267
- family_name: name.length > 1 ? name.at(-1) : void 0
268
- };
269
- const email = {
270
- email: user.email ?? void 0,
271
- email_verified: user.emailVerified ?? false
272
- };
273
- return {
274
- sub: user.id ?? void 0,
275
- ...scopes.includes("profile") ? profile : {},
276
- ...scopes.includes("email") ? email : {}
277
- };
278
- }
279
- /**
280
- * Handles the /oauth2/userinfo endpoint
281
- */
282
- async function userInfoEndpoint(ctx, opts) {
283
- const authorization = ctx.headers?.get("authorization");
284
- const token = typeof authorization === "string" && authorization?.startsWith("Bearer ") ? authorization?.replace("Bearer ", "") : authorization;
285
- if (!token?.length) throw new APIError("UNAUTHORIZED", {
286
- error_description: "authorization header not found",
287
- error: "invalid_request"
288
- });
289
- const jwt = await validateAccessToken(ctx, opts, token);
290
- const scopes = jwt.scope?.split(" ");
291
- if (!scopes?.includes("openid")) throw new APIError("BAD_REQUEST", {
292
- error_description: "Missing required scope",
293
- error: "invalid_scope"
294
- });
295
- if (!jwt.sub) throw new APIError("BAD_REQUEST", {
296
- error_description: "user not found",
297
- error: "invalid_request"
298
- });
299
- const user = await ctx.context.internalAdapter.findUserById(jwt.sub);
300
- if (!user) throw new APIError("BAD_REQUEST", {
301
- error_description: "user not found",
302
- error: "invalid_request"
303
- });
304
- const baseUserClaims = userNormalClaims(user, scopes ?? []);
305
- if (opts.pairwiseSecret) {
306
- const clientId = jwt.client_id ?? jwt.azp;
307
- if (clientId) {
308
- const client = await getClient(ctx, opts, clientId);
309
- if (client) baseUserClaims.sub = await resolveSubjectIdentifier(user.id, client, opts);
310
- }
311
- }
312
- const additionalInfoUserClaims = opts.customUserInfoClaims && scopes?.length ? await opts.customUserInfoClaims({
313
- user,
314
- scopes,
315
- jwt
316
- }) : {};
317
- return {
318
- ...baseUserClaims,
319
- ...additionalInfoUserClaims
320
- };
321
- }
322
- //#endregion
323
- //#region src/token.ts
324
- /**
325
- * Handles the /oauth2/token endpoint by delegating
326
- * the grant types
171
+ * The token reuses the ID Token signing key so any RP that validates ID Tokens
172
+ * from this OP can validate Logout Tokens without extra configuration. The
173
+ * caller resolves that key once and passes it in so a fan-out to many RPs does
174
+ * not re-read it per target.
175
+ *
176
+ * §2.4 mandates `iss`, `aud`, `iat`, `exp`, `jti`, `events`, and at least one
177
+ * of `sub` / `sid` (we send both). A `nonce` claim MUST NOT be present, and
178
+ * `alg: none` is forbidden (§2.6).
327
179
  */
328
- async function tokenEndpoint(ctx, opts) {
329
- const grantType = ctx.body.grant_type;
330
- if (opts.grantTypes && !opts.grantTypes.includes(grantType)) throw new APIError("BAD_REQUEST", {
331
- error_description: `unsupported grant_type ${grantType}`,
332
- error: "unsupported_grant_type"
333
- });
334
- switch (grantType) {
335
- case "authorization_code": return handleAuthorizationCodeGrant(ctx, opts);
336
- case "client_credentials": return handleClientCredentialsGrant(ctx, opts);
337
- case "refresh_token": return handleRefreshTokenGrant(ctx, opts);
338
- }
339
- }
340
- async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, resources, referenceId, overrides) {
341
- const iat = overrides?.iat ?? Math.floor(Date.now() / 1e3);
342
- const exp = overrides?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
343
- const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
344
- user,
345
- scopes,
346
- resources,
347
- referenceId,
348
- metadata: parseClientMetadata(client.metadata)
349
- }) : {};
350
- const jwtPluginOptions = getJwtPlugin(ctx.context).options;
180
+ async function signLogoutToken(ctx, options, resolvedKey, claims) {
351
181
  return signJWT(ctx, {
352
- options: jwtPluginOptions,
182
+ options,
353
183
  payload: {
354
- ...customClaims,
355
- sub: user?.id,
356
- aud: toAudienceClaim(audience),
357
- azp: client.clientId,
358
- scope: scopes.join(" "),
359
- sid: overrides?.sid,
360
- iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
361
- iat,
362
- exp
363
- }
364
- });
365
- }
366
- /**
367
- * Computes an OIDC hash (at_hash, c_hash) per OIDC Core §3.1.3.6.
368
- * Hashes the token, takes the left half, and base64url-encodes it.
369
- */
370
- async function computeOidcHash(token, signingAlg) {
371
- let hashAlg;
372
- if (signingAlg === "EdDSA") hashAlg = "SHA-512";
373
- else if (signingAlg.endsWith("384")) hashAlg = "SHA-384";
374
- else if (signingAlg.endsWith("512")) hashAlg = "SHA-512";
375
- else hashAlg = "SHA-256";
376
- const digest = new Uint8Array(await crypto.subtle.digest(hashAlg, new TextEncoder().encode(token)));
377
- return base64url.encode(digest.slice(0, digest.length / 2));
378
- }
379
- /**
380
- * Creates a user id token in code_authorization with scope of 'openid'
381
- * and hybrid/implicit (not yet implemented) flows
382
- */
383
- async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) {
384
- const iat = Math.floor(Date.now() / 1e3);
385
- const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
386
- const userClaims = userNormalClaims(user, scopes);
387
- const resolvedSub = await resolveSubjectIdentifier(user.id, client, opts);
388
- const authTimeSec = authTime != null ? Math.floor(authTime.getTime() / 1e3) : void 0;
389
- const acr = "urn:mace:incommon:iap:bronze";
390
- const customClaims = opts.customIdTokenClaims ? await opts.customIdTokenClaims({
391
- user,
392
- scopes,
393
- metadata: parseClientMetadata(client.metadata)
394
- }) : {};
395
- const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
396
- const resolvedKey = !opts.disableJwtPlugin && !jwtPluginOptions?.jwt?.sign ? await resolveSigningKey(ctx, jwtPluginOptions) : void 0;
397
- const signingAlg = opts.disableJwtPlugin ? "HS256" : resolvedKey?.alg ?? jwtPluginOptions?.jwks?.keyPairConfig?.alg;
398
- const atHash = accessToken ? await computeOidcHash(accessToken, signingAlg) : void 0;
399
- const payload = {
400
- ...userClaims,
401
- auth_time: authTimeSec,
402
- acr,
403
- ...customClaims,
404
- at_hash: atHash,
405
- iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
406
- sub: resolvedSub,
407
- aud: client.clientId,
408
- nonce,
409
- iat,
410
- exp,
411
- sid: client.enableEndSession ? sessionId : void 0
412
- };
413
- if (opts.disableJwtPlugin && !client.clientSecret) return;
414
- 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, {
415
- options: jwtPluginOptions,
416
- payload,
184
+ ...claims,
185
+ events: { [BACKCHANNEL_LOGOUT_EVENT_URI]: {} }
186
+ },
187
+ header: { typ: LOGOUT_TOKEN_JWT_TYP },
417
188
  resolvedKey: resolvedKey ?? void 0
418
189
  });
419
- if (idToken && atHash && jwtPluginOptions?.jwt?.sign) {
420
- const header = decodeProtectedHeader(idToken);
421
- if (header.alg !== signingAlg) throw new APIError("INTERNAL_SERVER_ERROR", {
422
- error_description: `ID token signed with "${header.alg}" but at_hash was computed for "${signingAlg}". Ensure jwt.sign uses the algorithm declared in keyPairConfig.alg.`,
423
- error: "server_error"
424
- });
425
- }
426
- return idToken;
427
190
  }
428
191
  /**
429
- * Encodes a refresh token for a client
430
- */
431
- async function encodeRefreshToken(opts, token, sessionId) {
432
- return (opts.prefix?.refreshToken ?? "") + (opts.formatRefreshToken?.encrypt ? opts.formatRefreshToken.encrypt(token, sessionId) : token);
433
- }
434
- /**
435
- * Decodes a refresh token for a client
192
+ * Synchronous phase: enumerate tokens for the session being terminated, revoke
193
+ * them, and return a plan for the asynchronous delivery phase. Runs inline in
194
+ * the `session.delete.before` hook so the DB state is consistent before the
195
+ * session row disappears.
436
196
  *
437
- * @internal
438
- */
439
- async function decodeRefreshToken(opts, token) {
440
- if (opts.prefix?.refreshToken) if (token.startsWith(opts.prefix.refreshToken)) token = token.replace(opts.prefix.refreshToken, "");
441
- else throw new APIError("BAD_REQUEST", {
442
- error_description: "refresh token not found",
443
- error: "invalid_token"
444
- });
445
- return opts.formatRefreshToken?.decrypt ? opts.formatRefreshToken?.decrypt(token) : { token };
446
- }
447
- async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, refreshId) {
448
- const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
449
- const exp = payload?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
450
- const token = opts.generateOpaqueAccessToken ? await opts.generateOpaqueAccessToken() : generateRandomString(32, "A-Z", "a-z");
451
- await ctx.context.adapter.create({
452
- model: "oauthAccessToken",
453
- data: {
454
- token: await storeToken(opts.storeTokens, token, "access_token"),
455
- clientId: client.clientId,
456
- sessionId: payload?.sid,
457
- userId: user?.id,
458
- referenceId,
459
- resources,
460
- refreshId,
461
- scopes,
462
- createdAt: /* @__PURE__ */ new Date(iat * 1e3),
463
- expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
464
- }
465
- });
466
- return (opts.prefix?.opaqueAccessToken ?? "") + token;
467
- }
468
- /**
469
- * Tear down the entire refresh-token family for a (client, user) pair, plus
470
- * any access tokens that reference those refresh rows, per RFC 9700 §4.14.
471
- * Access tokens are deleted first so the parent rows' foreign-key children
472
- * do not block the refresh-row delete.
197
+ * Revocation is the stored backstop, not the primary enforcement: introspection
198
+ * and `/userinfo` already treat a token whose session has ended as inactive
199
+ * (see `validateOpaqueAccessToken` / `validateJwtAccessToken`), so a missed
200
+ * `revoked` write cannot keep a session-bound token alive on its own. Access
201
+ * tokens bound to the session are revoked as OP hardening. Refresh tokens
202
+ * follow OIDC Back-Channel Logout 1.0 §2.7: those without `offline_access` are
203
+ * revoked; `offline_access` refresh tokens survive so long-lived API access can
204
+ * outlive the browser session.
473
205
  *
474
- * TODO(invalidate-family-race): the two `deleteMany` calls are not atomic
475
- * with respect to each other. Between them, a concurrent rotation in a
476
- * different worker can `create` a fresh refresh row (and, immediately after,
477
- * an access-token row referencing it) for the same (client, user) pair,
478
- * leaving the family partially rebuilt and the new refresh row orphaned of
479
- * any deletion. Closing this window requires the same transactional adapter
480
- * contract tracked under FIXME(strict-family-invalidation) in
481
- * `createRefreshToken`.
206
+ * Revocation runs regardless of the JWT plugin (refresh-token revocation has no
207
+ * dependency on signing). Only the Logout Token delivery plan needs the JWT
208
+ * plugin, so when it is disabled we still revoke but never build a plan.
482
209
  *
483
- * @internal
210
+ * Returns `null` when there is nothing to do, so the caller can skip the
211
+ * background handoff entirely.
484
212
  */
485
- async function invalidateRefreshFamily(ctx, clientId, userId) {
486
- const refreshTokens = await ctx.context.adapter.findMany({
487
- model: "oauthRefreshToken",
488
- where: [{
489
- field: "clientId",
490
- value: clientId
491
- }, {
492
- field: "userId",
493
- value: userId
494
- }]
495
- });
496
- if (refreshTokens.length) await ctx.context.adapter.deleteMany({
497
- model: "oauthAccessToken",
498
- where: [{
499
- field: "refreshId",
500
- operator: "in",
501
- value: refreshTokens.map((r) => r.id)
502
- }]
503
- });
504
- await ctx.context.adapter.deleteMany({
505
- model: "oauthRefreshToken",
506
- where: [{
507
- field: "clientId",
508
- value: clientId
509
- }, {
510
- field: "userId",
511
- value: userId
512
- }]
513
- });
514
- }
515
- async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, payload, originalRefresh, authTime, resources) {
516
- const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
517
- const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
518
- const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
519
- const sessionId = payload?.sid;
520
- const newRow = {
521
- token: await storeToken(opts.storeTokens, token, "refresh_token"),
522
- clientId: client.clientId,
523
- sessionId,
524
- userId: user.id,
525
- referenceId,
526
- authTime,
527
- scopes,
528
- resources,
529
- createdAt: /* @__PURE__ */ new Date(iat * 1e3),
530
- expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
531
- };
532
- if (!originalRefresh?.id) return {
533
- id: (await ctx.context.adapter.create({
534
- model: "oauthRefreshToken",
535
- data: newRow
536
- })).id,
537
- token: await encodeRefreshToken(opts, token, sessionId)
538
- };
539
- if (!await ctx.context.adapter.update({
540
- model: "oauthRefreshToken",
541
- where: [{
542
- field: "id",
543
- value: originalRefresh.id
544
- }, {
545
- field: "revoked",
546
- operator: "eq",
547
- value: null
548
- }],
549
- update: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
550
- })) throw new APIError("BAD_REQUEST", {
551
- error_description: "invalid refresh token",
552
- error: "invalid_grant"
553
- });
554
- return {
555
- id: (await ctx.context.adapter.create({
556
- model: "oauthRefreshToken",
557
- data: newRow
558
- })).id,
559
- token: await encodeRefreshToken(opts, token, sessionId)
560
- };
561
- }
562
- async function createUserTokens(ctx, opts, params) {
563
- const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, verificationValue } = params;
564
- const iat = Math.floor(Date.now() / 1e3);
565
- const defaultExp = iat + (user ? opts.accessTokenExpiresIn ?? 3600 : opts.m2mAccessTokenExpiresIn ?? 3600);
566
- const exp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
567
- return prev < curr ? prev : curr;
568
- }, defaultExp) : defaultExp;
569
- const resourceResult = await checkResource(ctx, opts, params?.resources, scopes);
570
- if (!resourceResult.success) throw new APIError("BAD_REQUEST", {
571
- error_description: "requested resource invalid",
572
- error: "invalid_target"
573
- });
574
- const audience = resourceResult.audience;
575
- const isRefreshToken = user && (existingRefreshToken?.scopes?.includes("offline_access") || scopes.includes("offline_access"));
576
- const isJwtAccessToken = audience && !opts.disableJwtPlugin;
577
- const isIdToken = user && scopes.includes("openid");
578
- const customFields = opts.customTokenResponseFields ? await opts.customTokenResponseFields({
579
- grantType,
580
- user,
581
- scopes,
582
- metadata: parseClientMetadata(client.metadata),
583
- verificationValue
584
- }) : void 0;
585
- const refreshResources = params?.refreshToken?.resources ?? params?.originalResources ?? params?.resources;
586
- const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
587
- iat,
588
- exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
589
- sid: sessionId
590
- }, existingRefreshToken, authTime, refreshResources) : void 0;
591
- const [accessToken, refreshToken] = await Promise.all([isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, params?.resources, referenceId, {
592
- iat,
593
- exp,
594
- sid: sessionId
595
- }) : createOpaqueAccessToken(ctx, opts, user, client, scopes, {
596
- iat,
597
- exp,
598
- sid: sessionId
599
- }, params?.resources, referenceId, earlyRefreshToken?.id), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
600
- iat,
601
- exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
602
- sid: sessionId
603
- }, existingRefreshToken, authTime, refreshResources) : void 0]);
604
- const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) : void 0;
605
- return ctx.json({
606
- ...customFields,
607
- access_token: accessToken,
608
- expires_in: exp - iat,
609
- expires_at: exp,
610
- token_type: "Bearer",
611
- refresh_token: refreshToken?.token,
612
- scope: scopes.join(" "),
613
- id_token: idToken
614
- }, { headers: {
615
- "Cache-Control": "no-store",
616
- Pragma: "no-cache"
617
- } });
618
- }
619
- /** Checks verification value */
620
- async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resource) {
621
- const verification = await ctx.context.internalAdapter.consumeVerificationValue(await storeToken(opts.storeTokens, code, "authorization_code"));
622
- if (!verification) throw new APIError("UNAUTHORIZED", {
623
- error_description: "invalid code",
624
- error: "invalid_grant"
625
- });
626
- let rawValue;
213
+ async function revokeAndPlanBackchannelLogout(ctx, opts, input) {
214
+ const { sessionId, userId } = input;
215
+ if (!userId) return null;
216
+ const logger = ctx.context.logger;
627
217
  try {
628
- rawValue = JSON.parse(verification.value);
629
- } catch {
630
- throw new APIError("UNAUTHORIZED", {
631
- error_description: "malformed verification value",
632
- error: "invalid_grant"
633
- });
634
- }
635
- const parsed = verificationValueSchema.safeParse(rawValue);
636
- if (!parsed.success) throw new APIError("UNAUTHORIZED", {
637
- error_description: "malformed verification value",
638
- error: "invalid_grant"
639
- });
640
- const verificationValue = parsed.data;
641
- if (verificationValue.query.client_id !== client_id) throw new APIError("UNAUTHORIZED", {
642
- error_description: "invalid client_id",
643
- error: "invalid_client"
644
- });
645
- if (verificationValue.query?.redirect_uri && verificationValue.query?.redirect_uri !== redirect_uri) throw new APIError("BAD_REQUEST", {
646
- error_description: "redirect_uri mismatch",
647
- error: "invalid_request"
648
- });
649
- const storedResources = toResourceList(verificationValue.resource) ?? toResourceList(verificationValue.query.resource);
650
- const effectiveResources = resource ?? storedResources;
651
- if (resource && storedResources) {
652
- const requestedSet = new Set(resource);
653
- const authorizedSet = new Set(storedResources);
654
- for (const r of requestedSet) if (!authorizedSet.has(r)) throw new APIError("BAD_REQUEST", {
655
- error_description: "requested resource not authorized",
656
- error: "invalid_target"
657
- });
658
- }
659
- return {
660
- verificationValue,
661
- effectiveResources,
662
- authorizedResources: storedResources
663
- };
664
- }
665
- /**
666
- * Obtains new Session Jwt and Refresh Tokens using a code
667
- */
668
- async function handleAuthorizationCodeGrant(ctx, opts) {
669
- const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
670
- const { code, code_verifier, redirect_uri, resource } = ctx.body;
671
- const resources = toResourceList(resource);
672
- if (!client_id) throw new APIError("BAD_REQUEST", {
673
- error_description: "client_id is required",
674
- error: "invalid_request"
675
- });
676
- if (!code) throw new APIError("BAD_REQUEST", {
677
- error_description: "code is required",
678
- error: "invalid_request"
679
- });
680
- if (!redirect_uri) throw new APIError("BAD_REQUEST", {
681
- error_description: "redirect_uri is required",
682
- error: "invalid_request"
683
- });
684
- const isAuthCodeWithSecret = client_id && client_secret;
685
- const isAuthCodeWithPkce = client_id && code && code_verifier;
686
- if (!isAuthCodeWithSecret && !isAuthCodeWithPkce && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
687
- error_description: "Either code_verifier or client_secret is required",
688
- error: "invalid_request"
689
- });
690
- /** Get and check Verification Value */
691
- const { verificationValue, effectiveResources, authorizedResources } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
692
- const scopes = verificationValue.query.scope?.split(" ");
693
- if (!scopes) throw new APIError("INTERNAL_SERVER_ERROR", {
694
- error_description: "verification scope unset",
695
- error: "invalid_scope"
696
- });
697
- /** Verify Client */
698
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerifiedClient);
699
- if (isPKCERequired(client, (verificationValue.query?.scope)?.split(" ") || [])) {
700
- if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
701
- error_description: "PKCE is required for this client",
702
- error: "invalid_request"
703
- });
704
- } else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret || preVerifiedClient)) throw new APIError("BAD_REQUEST", {
705
- error_description: "Either PKCE (code_verifier) or client authentication (client_secret or client_assertion) is required",
706
- error: "invalid_request"
707
- });
708
- /** Check PKCE challenge if verifier is provided */
709
- const pkceUsedInAuth = !!verificationValue.query?.code_challenge;
710
- const pkceUsedInToken = !!code_verifier;
711
- if (pkceUsedInAuth || pkceUsedInToken) {
712
- if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
713
- error_description: "code_verifier required because PKCE was used in authorization",
714
- error: "invalid_request"
715
- });
716
- if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
717
- error_description: "code_verifier provided but PKCE was not used in authorization",
718
- error: "invalid_request"
719
- });
720
- if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("UNAUTHORIZED", {
721
- error_description: "code verification failed",
722
- error: "invalid_request"
723
- });
724
- }
725
- /** Get user */
726
- if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
727
- error_description: "missing user, user may have been deleted",
728
- error: "invalid_user"
729
- });
730
- const user = await ctx.context.internalAdapter.findUserById(verificationValue.userId);
731
- if (!user) throw new APIError("BAD_REQUEST", {
732
- error_description: "missing user, user may have been deleted",
733
- error: "invalid_user"
734
- });
735
- const session = await ctx.context.adapter.findOne({
736
- model: "session",
737
- where: [{
738
- field: "id",
739
- value: verificationValue.sessionId
740
- }]
741
- });
742
- if (!session || session.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
743
- error_description: "session no longer exists",
744
- error: "invalid_request"
745
- });
746
- const authTime = verificationValue.authTime != null ? normalizeTimestampValue(verificationValue.authTime) : resolveSessionAuthTime(session);
747
- return createUserTokens(ctx, opts, {
748
- client,
749
- scopes: verificationValue.query.scope?.split(" ") ?? [],
750
- user,
751
- grantType: "authorization_code",
752
- referenceId: verificationValue.referenceId,
753
- sessionId: session.id,
754
- nonce: verificationValue.query?.nonce,
755
- authTime,
756
- verificationValue,
757
- resources: effectiveResources,
758
- originalResources: authorizedResources
759
- });
760
- }
761
- /**
762
- * Grant that allows direct access to an API using the application's credentials
763
- * This grant is for M2M so the concept of a user id does not exist on the token.
764
- *
765
- * MUST follow https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
766
- */
767
- async function handleClientCredentialsGrant(ctx, opts) {
768
- const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
769
- const { scope, resource } = ctx.body;
770
- const resources = toResourceList(resource);
771
- if (!client_id) throw new APIError("BAD_REQUEST", {
772
- error_description: "Missing required client_id",
773
- error: "invalid_grant"
774
- });
775
- if (!client_secret && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
776
- error_description: "Missing a required client_secret",
777
- error: "invalid_grant"
778
- });
779
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
780
- let requestedScopes = scope?.split(" ");
781
- if (requestedScopes) {
782
- const validScopes = new Set(client.scopes ?? opts.scopes);
783
- const oidcScopes = new Set([
784
- "openid",
785
- "profile",
786
- "email",
787
- "offline_access"
788
- ]);
789
- const invalidScopes = requestedScopes.filter((scope) => {
790
- return !validScopes?.has(scope) || oidcScopes.has(scope);
791
- });
792
- if (invalidScopes.length) throw new APIError("BAD_REQUEST", {
793
- error_description: `The following scopes are invalid: ${invalidScopes.join(", ")}`,
794
- error: "invalid_scope"
795
- });
796
- }
797
- if (!requestedScopes) requestedScopes = client.scopes ?? opts.clientCredentialGrantDefaultScopes ?? opts.scopes ?? [];
798
- return createUserTokens(ctx, opts, {
799
- client,
800
- scopes: requestedScopes,
801
- grantType: "client_credentials",
802
- resources
803
- });
804
- }
805
- /**
806
- * Obtains new Session Jwt and Refresh Tokens using a refresh token
807
- *
808
- * Refresh tokens will only allow the same or lesser scopes as the initial authorize request.
809
- * To add scopes, you must restart the authorize process again.
810
- */
811
- async function handleRefreshTokenGrant(ctx, opts) {
812
- const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
813
- const { refresh_token, scope, resource } = ctx.body;
814
- const resources = toResourceList(resource);
815
- if (!client_id) throw new APIError("BAD_REQUEST", {
816
- error_description: "Missing required client_id",
817
- error: "invalid_grant"
818
- });
819
- if (!refresh_token) throw new APIError("BAD_REQUEST", {
820
- error_description: "Missing a required refresh_token for refresh_token grant",
821
- error: "invalid_grant"
822
- });
823
- const decodedRefresh = await decodeRefreshToken(opts, refresh_token);
824
- const refreshToken = await ctx.context.adapter.findOne({
825
- model: "oauthRefreshToken",
826
- where: [{
827
- field: "token",
828
- value: await getStoredToken(opts.storeTokens, decodedRefresh.token, "refresh_token")
829
- }]
830
- });
831
- if (!refreshToken) throw new APIError("BAD_REQUEST", {
832
- error_description: "session not found",
833
- error: "invalid_grant"
834
- });
835
- if (refreshToken.clientId !== client_id) throw new APIError("BAD_REQUEST", {
836
- error_description: "invalid client_id",
837
- error: "invalid_client"
838
- });
839
- if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
840
- error_description: "invalid refresh token",
841
- error: "invalid_grant"
842
- });
843
- if (refreshToken.revoked) {
844
- await invalidateRefreshFamily(ctx, client_id, refreshToken.userId);
845
- throw new APIError("BAD_REQUEST", {
846
- error_description: "invalid refresh token",
847
- error: "invalid_grant"
848
- });
849
- }
850
- if (resources && refreshToken.resources && !resources.every((v) => refreshToken.resources?.includes(v))) throw new APIError("BAD_REQUEST", {
851
- error_description: "requested resource invalid",
852
- error: "invalid_target"
853
- });
854
- const scopes = refreshToken?.scopes;
855
- const requestedScopes = scope?.split(" ");
856
- if (requestedScopes) {
857
- const validScopes = new Set(scopes);
858
- for (const requestedScope of requestedScopes) if (!validScopes.has(requestedScope)) throw new APIError("BAD_REQUEST", {
859
- error_description: `unable to issue scope ${requestedScope}`,
860
- error: "invalid_scope"
861
- });
862
- }
863
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes, preVerifiedClient);
864
- const user = await ctx.context.internalAdapter.findUserById(refreshToken.userId);
865
- if (!user) throw new APIError("BAD_REQUEST", {
866
- error_description: "user not found",
867
- error: "invalid_request"
868
- });
869
- const authTime = refreshToken.authTime != null ? normalizeTimestampValue(refreshToken.authTime) : void 0;
870
- return createUserTokens(ctx, opts, {
871
- client,
872
- scopes: requestedScopes ?? scopes,
873
- user,
874
- grantType: "refresh_token",
875
- referenceId: refreshToken.referenceId,
876
- sessionId: refreshToken.sessionId,
877
- refreshToken,
878
- resources: resources ?? refreshToken.resources,
879
- authTime
880
- });
881
- }
882
- //#endregion
883
- //#region src/introspect.ts
884
- /**
885
- * IMPORTANT NOTES:
886
- * Introspection follows RFC7662
887
- * https://datatracker.ietf.org/doc/html/rfc7662
888
- * - APIError: Continue catches (returnable to client)
889
- * - Error: Should immediately stop catches (internal error)
890
- */
891
- /**
892
- * Validates a JWT access token against the configured JWKs.
893
- *
894
- * @returns RFC7662 introspection format
895
- */
896
- async function validateJwtAccessToken(ctx, opts, token, clientId) {
897
- const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
898
- const jwtPluginOptions = jwtPlugin?.options;
899
- let jwtPayload;
900
- try {
901
- jwtPayload = await verifyJwsAccessToken(token, {
902
- jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
903
- return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
904
- },
905
- verifyOptions: {
906
- audience: opts.validAudiences ?? ctx.context.baseURL,
907
- issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL
908
- }
909
- });
910
- } catch (error) {
911
- if (error instanceof Error) {
912
- if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
913
- error_description: "invalid JWT signature",
914
- error: "invalid_request"
915
- });
916
- else if (error.name === "JWTExpired") return { active: false };
917
- else if (error.name === "JWTInvalid") return { active: false };
918
- throw error;
919
- }
920
- throw new Error(error);
921
- }
922
- let client;
923
- if (jwtPayload.azp) {
924
- client = await getClient(ctx, opts, jwtPayload.azp);
925
- if (!client || client?.disabled) return { active: false };
926
- if (clientId && jwtPayload.azp !== clientId) return { active: false };
927
- }
928
- const sessionId = jwtPayload.sid;
929
- if (sessionId) {
930
- const session = await ctx.context.adapter.findOne({
931
- model: "session",
218
+ const where = [{
219
+ field: "sessionId",
220
+ value: sessionId
221
+ }];
222
+ const [accessTokens, refreshTokens] = await Promise.all([ctx.context.adapter.findMany({
223
+ model: "oauthAccessToken",
224
+ where
225
+ }), ctx.context.adapter.findMany({
226
+ model: "oauthRefreshToken",
227
+ where
228
+ })]);
229
+ const affectedClientIds = /* @__PURE__ */ new Set();
230
+ for (const t of accessTokens) affectedClientIds.add(t.clientId);
231
+ for (const t of refreshTokens) affectedClientIds.add(t.clientId);
232
+ if (affectedClientIds.size === 0) return null;
233
+ const clients = await ctx.context.adapter.findMany({
234
+ model: "oauthClient",
932
235
  where: [{
933
- field: "id",
934
- value: sessionId
236
+ field: "clientId",
237
+ operator: "in",
238
+ value: Array.from(affectedClientIds)
935
239
  }]
936
240
  });
937
- if (!session || session.expiresAt < /* @__PURE__ */ new Date()) jwtPayload.sid = void 0;
938
- }
939
- if (jwtPayload.azp) jwtPayload.client_id = jwtPayload.azp;
940
- jwtPayload.active = true;
941
- return jwtPayload;
942
- }
943
- /**
944
- * Searches for an opaque access token in the database and validates it
945
- *
946
- * @returns RFC7662 introspection format
947
- */
948
- async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
949
- let tokenValue = token;
950
- if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
951
- else throw new APIError$1("BAD_REQUEST", {
952
- error_description: "opaque access token not found",
953
- error: "invalid_request"
954
- });
955
- const accessToken = await ctx.context.adapter.findOne({
956
- model: "oauthAccessToken",
957
- where: [{
958
- field: "token",
959
- value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
960
- }]
961
- });
962
- if (!accessToken) throw new APIError$1("BAD_REQUEST", {
963
- error_description: "opaque access token not found",
964
- error: "invalid_token"
965
- });
966
- if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
967
- let client;
968
- if (accessToken.clientId) {
969
- client = await getClient(ctx, opts, accessToken.clientId);
970
- if (!client || client?.disabled) return { active: false };
971
- if (clientId && accessToken.clientId !== clientId) return { active: false };
972
- }
973
- let sessionId = accessToken.sessionId ?? void 0;
974
- if (sessionId) {
975
- const session = await ctx.context.adapter.findOne({
976
- model: "session",
241
+ const revokedAt = /* @__PURE__ */ new Date();
242
+ const accessToRevokeIds = accessTokens.filter((t) => !t.revoked).map((t) => t.id);
243
+ const refreshToRevokeIds = refreshTokens.filter((t) => !t.revoked && !t.scopes?.includes("offline_access")).map((t) => t.id);
244
+ const revocations = await Promise.allSettled([accessToRevokeIds.length > 0 ? ctx.context.adapter.updateMany({
245
+ model: "oauthAccessToken",
977
246
  where: [{
978
247
  field: "id",
979
- value: sessionId
980
- }]
981
- });
982
- if (!session || session.expiresAt < /* @__PURE__ */ new Date()) sessionId = void 0;
983
- }
984
- let user;
985
- if (accessToken.userId) user = await ctx.context.internalAdapter.findUserById(accessToken?.userId);
986
- const resources = Array.isArray(accessToken.resources) ? accessToken.resources : void 0;
987
- const audience = resources ? [...resources] : void 0;
988
- if (audience?.length && accessToken.scopes?.includes("openid")) {
989
- const userInfoEndpoint = `${ctx.context.baseURL}/oauth2/userinfo`;
990
- if (!audience.includes(userInfoEndpoint)) audience.push(userInfoEndpoint);
991
- }
992
- const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
993
- user,
994
- scopes: accessToken.scopes,
995
- referenceId: accessToken?.referenceId,
996
- resources,
997
- metadata: parseClientMetadata(client?.metadata)
998
- }) : {};
999
- const jwtPluginOptions = (opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options;
1000
- return {
1001
- ...customClaims,
1002
- active: true,
1003
- iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
1004
- aud: toAudienceClaim(audience),
1005
- client_id: accessToken.clientId,
1006
- sub: user?.id,
1007
- sid: sessionId,
1008
- exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
1009
- iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
1010
- scope: accessToken.scopes?.join(" ")
1011
- };
1012
- }
1013
- /**
1014
- * Validates a refresh token in the session store.
1015
- *
1016
- * @returns payload in RFC7662 introspection format
1017
- */
1018
- async function validateRefreshToken(ctx, opts, token, clientId) {
1019
- const refreshToken = await ctx.context.adapter.findOne({
1020
- model: "oauthRefreshToken",
1021
- where: [{
1022
- field: "token",
1023
- value: await getStoredToken(opts.storeTokens, token, "refresh_token")
1024
- }]
1025
- });
1026
- if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
1027
- error_description: "token not found",
1028
- error: "invalid_token"
1029
- });
1030
- if (!refreshToken.clientId || refreshToken.clientId !== clientId) return { active: false };
1031
- if (!refreshToken.expiresAt || refreshToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
1032
- if (refreshToken.revoked) return { active: false };
1033
- let sessionId = refreshToken.sessionId ?? void 0;
1034
- if (sessionId) {
1035
- const session = await ctx.context.adapter.findOne({
1036
- model: "session",
248
+ operator: "in",
249
+ value: accessToRevokeIds
250
+ }],
251
+ update: { revoked: revokedAt }
252
+ }) : Promise.resolve(), refreshToRevokeIds.length > 0 ? ctx.context.adapter.updateMany({
253
+ model: "oauthRefreshToken",
1037
254
  where: [{
1038
255
  field: "id",
1039
- value: sessionId
1040
- }]
1041
- });
1042
- if (!session || session.expiresAt < /* @__PURE__ */ new Date()) sessionId = void 0;
256
+ operator: "in",
257
+ value: refreshToRevokeIds
258
+ }],
259
+ update: { revoked: revokedAt }
260
+ }) : Promise.resolve()]);
261
+ for (const result of revocations) if (result.status === "rejected") logger.error("back-channel logout: token revocation update failed", result.reason);
262
+ const eligibleClients = opts.disableJwtPlugin ? [] : clients.filter((c) => Boolean(c.backchannelLogoutUri) && !c.disabled);
263
+ if (eligibleClients.length === 0) return null;
264
+ return {
265
+ sessionId,
266
+ targets: await Promise.all(eligibleClients.map(async (client) => ({
267
+ client,
268
+ sub: await resolveSubjectIdentifier(userId, client, opts)
269
+ })))
270
+ };
271
+ } catch (error) {
272
+ logger.error("back-channel logout revocation failed", error);
273
+ return null;
1043
274
  }
1044
- let user = void 0;
1045
- if (refreshToken.userId) user = await ctx.context.internalAdapter.findUserById(refreshToken?.userId) ?? void 0;
1046
- return {
1047
- active: true,
1048
- client_id: clientId,
1049
- iss: ((opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options)?.jwt?.issuer ?? ctx.context.baseURL,
1050
- sub: user?.id,
1051
- sid: sessionId,
1052
- exp: Math.floor(new Date(refreshToken.expiresAt).getTime() / 1e3),
1053
- iat: Math.floor(new Date(refreshToken.createdAt).getTime() / 1e3),
1054
- scope: refreshToken.scopes?.join(" ")
1055
- };
1056
275
  }
1057
276
  /**
1058
- * We don't know the access token format so we try to validate it
1059
- * as a JWT first, then as an opaque token.
277
+ * Asynchronous phase: sign one Logout Token per target client and POST it to
278
+ * the registered `backchannel_logout_uri`. The caller hands this to
279
+ * `runInBackgroundOrAwait`, so when a background handler is configured (Vercel
280
+ * `waitUntil`, Cloudflare `ctx.waitUntil`) it runs after the response; without
281
+ * one it completes inline so delivery is not lost on request teardown.
1060
282
  *
1061
- * @returns RFC7662 introspection format
1062
- *
1063
- * @internal
283
+ * Spec §2.5: "the OP SHOULD NOT retransmit", so each RP gets a single attempt
284
+ * within `BACKCHANNEL_DISPATCH_TIMEOUT_MS`. Every per-client failure (fetch
285
+ * error, non-2xx response, signing error, subject resolution error) is
286
+ * logged; none of them can reject the outer promise.
1064
287
  */
1065
- async function validateAccessToken(ctx, opts, token, clientId) {
1066
- try {
1067
- return await validateJwtAccessToken(ctx, opts, token, clientId);
1068
- } catch (err) {
1069
- if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
1070
- else throw new Error(err);
1071
- }
1072
- try {
1073
- return await validateOpaqueAccessToken(ctx, opts, token, clientId);
1074
- } catch (err) {
1075
- if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
1076
- else throw new Error("Unknown error validating access token");
1077
- }
1078
- throw new APIError$1("BAD_REQUEST", {
1079
- error_description: "Invalid access token",
1080
- error: "invalid_request"
1081
- });
1082
- }
1083
- /**
1084
- * Resolves pairwise sub on an introspection payload.
1085
- * Applied at the presentation layer so internal validation functions
1086
- * keep real user.id (needed for user lookup in /userinfo).
1087
- */
1088
- async function resolveIntrospectionSub(opts, payload, client) {
1089
- if (payload.active && payload.sub) {
1090
- const resolvedSub = await resolveSubjectIdentifier(payload.sub, client, opts);
1091
- return {
1092
- ...payload,
1093
- sub: resolvedSub
1094
- };
1095
- }
1096
- return payload;
1097
- }
1098
- async function introspectEndpoint(ctx, opts) {
1099
- let { token, token_type_hint } = ctx.body;
1100
- if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
1101
- const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
1102
- if (!client_id || !client_secret && !preVerifiedClient) throw new APIError$1("UNAUTHORIZED", {
1103
- error_description: "missing required credentials",
1104
- error: "invalid_client"
1105
- });
1106
- if (token && typeof token === "string" && token.startsWith("Bearer ")) token = token.replace("Bearer ", "");
1107
- if (!token?.length) throw new APIError$1("BAD_REQUEST", {
1108
- error_description: "missing a required token for introspection",
1109
- error: "invalid_request"
1110
- });
1111
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
1112
- try {
1113
- if (token_type_hint === void 0 || token_type_hint === "access_token") try {
1114
- return resolveIntrospectionSub(opts, await validateAccessToken(ctx, opts, token, client.clientId), client);
1115
- } catch (error) {
1116
- if (error instanceof APIError$1) {
1117
- if (token_type_hint === "access_token") throw error;
1118
- } else if (error instanceof Error) throw error;
1119
- else throw new Error(error);
1120
- }
1121
- if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
1122
- return resolveIntrospectionSub(opts, await validateRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId), client);
288
+ async function deliverBackchannelLogoutTokens(ctx, plan) {
289
+ const logger = ctx.context.logger;
290
+ const jwtPluginOptions = getJwtPlugin(ctx.context)?.options;
291
+ const iss = jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL;
292
+ const iat = Math.floor(Date.now() / 1e3);
293
+ const exp = iat + LOGOUT_TOKEN_LIFETIME_SECONDS;
294
+ const resolvedKey = jwtPluginOptions?.jwt?.sign ? null : await resolveSigningKey(ctx, jwtPluginOptions);
295
+ await Promise.allSettled(plan.targets.map(async ({ client, sub }) => {
296
+ try {
297
+ const jti = generateRandomString(32, "a-z", "A-Z", "0-9");
298
+ const token = await signLogoutToken(ctx, jwtPluginOptions, resolvedKey, {
299
+ iss,
300
+ aud: client.clientId,
301
+ sub,
302
+ sid: plan.sessionId,
303
+ iat,
304
+ exp,
305
+ jti
306
+ });
307
+ const response = await fetch(client.backchannelLogoutUri, {
308
+ method: "POST",
309
+ headers: {
310
+ "Content-Type": "application/x-www-form-urlencoded",
311
+ Accept: "application/json"
312
+ },
313
+ body: new URLSearchParams({ logout_token: token }),
314
+ signal: AbortSignal.timeout(BACKCHANNEL_DISPATCH_TIMEOUT_MS),
315
+ redirect: "error"
316
+ });
317
+ if (response.status !== 200 && response.status !== 204) logger.warn(`back-channel logout to client ${client.clientId} returned ${response.status}`);
1123
318
  } catch (error) {
1124
- if (error instanceof APIError$1) {
1125
- if (token_type_hint === "refresh_token") throw error;
1126
- } else if (error instanceof Error) throw error;
1127
- else throw new Error(error);
1128
- }
1129
- throw new APIError$1("BAD_REQUEST", {
1130
- error_description: "token not found",
1131
- error: "invalid_request"
1132
- });
1133
- } catch (error) {
1134
- if (error instanceof APIError$1) {
1135
- if (error.name === "BAD_REQUEST") return { active: false };
1136
- throw error;
1137
- } else if (error instanceof Error) {
1138
- logger.error("Introspection error:", error.message, error.stack);
1139
- throw new APIError$1("INTERNAL_SERVER_ERROR");
1140
- } else {
1141
- logger.error("Introspection error:", error);
1142
- throw new APIError$1("INTERNAL_SERVER_ERROR");
319
+ logger.warn(`back-channel logout to client ${client.clientId} failed`, error);
1143
320
  }
1144
- }
321
+ }));
1145
322
  }
1146
- //#endregion
1147
- //#region src/logout.ts
1148
323
  /**
1149
- * IMPORTANT NOTES:
1150
- * Follows OIDC RP-Initiated Logout
324
+ * RP-Initiated Logout (OIDC RP-Initiated Logout 1.0). The RP presents a signed
325
+ * `id_token_hint`; after verification, the OP terminates the matching session
326
+ * and optionally redirects to `post_logout_redirect_uri`.
327
+ *
328
+ * Session termination goes through `internalAdapter.deleteSession`, which fires
329
+ * `session.delete.before` so the hook drives revocation and back-channel
330
+ * notifications to every RP with tokens on the session.
1151
331
  *
1152
332
  * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html
1153
333
  */
@@ -1195,12 +375,10 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
1195
375
  });
1196
376
  const secret = await decryptStoredClientSecret(ctx, opts.storeClientSecret, clientSecret);
1197
377
  const { payload } = await compactVerify(id_token_hint, new TextEncoder().encode(secret));
1198
- const idToken = new TextDecoder().decode(payload);
1199
- idTokenPayload = JSON.parse(idToken);
378
+ idTokenPayload = JSON.parse(new TextDecoder().decode(payload));
1200
379
  } else {
1201
380
  const { payload } = await compactVerify(id_token_hint, createLocalJWKSet(await getJwks(id_token_hint, { jwksFetch: jwksUrl })));
1202
- const idToken = new TextDecoder().decode(payload);
1203
- idTokenPayload = JSON.parse(idToken);
381
+ idTokenPayload = JSON.parse(new TextDecoder().decode(payload));
1204
382
  }
1205
383
  if (!idTokenPayload) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1206
384
  error_description: "missing payload",
@@ -1232,18 +410,13 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
1232
410
  value: sessionId
1233
411
  }]
1234
412
  });
1235
- session?.token ? await ctx.context.internalAdapter.deleteSession(session?.token) : session?.id ? await ctx.context.adapter.delete({
413
+ if (session?.token) await ctx.context.internalAdapter.deleteSession(session.token);
414
+ else if (session) await ctx.context.adapter.delete({
1236
415
  model: "session",
1237
416
  where: [{
1238
417
  field: "id",
1239
418
  value: session.id
1240
419
  }]
1241
- }) : await ctx.context.adapter.delete({
1242
- model: "session",
1243
- where: [{
1244
- field: "id",
1245
- value: sessionId
1246
- }]
1247
420
  });
1248
421
  } catch {}
1249
422
  if (post_logout_redirect_uri) {
@@ -1255,6 +428,155 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
1255
428
  }
1256
429
  }
1257
430
  //#endregion
431
+ //#region src/metadata.ts
432
+ function authServerMetadata(ctx, opts, overrides) {
433
+ const baseURL = ctx.context.baseURL;
434
+ const backchannelSupported = !overrides?.jwt_disabled;
435
+ return {
436
+ scopes_supported: overrides?.scopes_supported,
437
+ issuer: validateIssuerUrl(opts?.jwt?.issuer ?? baseURL),
438
+ authorization_endpoint: `${baseURL}/oauth2/authorize`,
439
+ token_endpoint: `${baseURL}/oauth2/token`,
440
+ jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
441
+ registration_endpoint: overrides?.dynamic_client_registration_supported ? `${baseURL}/oauth2/register` : void 0,
442
+ introspection_endpoint: `${baseURL}/oauth2/introspect`,
443
+ revocation_endpoint: `${baseURL}/oauth2/revoke`,
444
+ response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
445
+ response_modes_supported: ["query"],
446
+ grant_types_supported: overrides?.grant_types_supported ?? [
447
+ "authorization_code",
448
+ "client_credentials",
449
+ "refresh_token"
450
+ ],
451
+ token_endpoint_auth_methods_supported: overrides?.token_endpoint_auth_methods_supported ?? [
452
+ ...overrides?.public_client_supported ? ["none"] : [],
453
+ "client_secret_basic",
454
+ "client_secret_post",
455
+ "private_key_jwt"
456
+ ],
457
+ token_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
458
+ introspection_endpoint_auth_methods_supported: overrides?.endpoint_auth_methods_supported ?? [
459
+ "client_secret_basic",
460
+ "client_secret_post",
461
+ "private_key_jwt"
462
+ ],
463
+ introspection_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
464
+ revocation_endpoint_auth_methods_supported: overrides?.endpoint_auth_methods_supported ?? [
465
+ "client_secret_basic",
466
+ "client_secret_post",
467
+ "private_key_jwt"
468
+ ],
469
+ revocation_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
470
+ code_challenge_methods_supported: ["S256"],
471
+ authorization_response_iss_parameter_supported: true,
472
+ dpop_signing_alg_values_supported: overrides?.dpop_signing_alg_values_supported ?? [...DPOP_SIGNING_ALGORITHMS],
473
+ backchannel_logout_supported: backchannelSupported,
474
+ backchannel_logout_session_supported: backchannelSupported
475
+ };
476
+ }
477
+ /**
478
+ * Builds the authorization-server metadata shared by the
479
+ * `/.well-known/oauth-authorization-server` and `/.well-known/openid-configuration`
480
+ * responses, plus the inputs both need to finish their own document.
481
+ */
482
+ function buildAuthServerMetadata(ctx, opts) {
483
+ const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
484
+ const clientDiscoveries = getClientDiscoveries(opts);
485
+ const publicClientSupported = opts.allowUnauthenticatedClientRegistration || clientDiscoveries.length > 0;
486
+ return {
487
+ jwtPluginOptions,
488
+ clientDiscoveries,
489
+ authMetadata: authServerMetadata(ctx, jwtPluginOptions, {
490
+ scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
491
+ dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
492
+ public_client_supported: publicClientSupported,
493
+ grant_types_supported: getSupportedGrantTypes(opts),
494
+ token_endpoint_auth_methods_supported: getSupportedAuthMethods(opts, { includeNone: publicClientSupported }),
495
+ endpoint_auth_methods_supported: getSupportedAuthMethods(opts),
496
+ jwt_disabled: opts.disableJwtPlugin,
497
+ dpop_signing_alg_values_supported: opts.dpop?.signingAlgorithms
498
+ })
499
+ };
500
+ }
501
+ function oauthAuthorizationServerMetadata(ctx, opts) {
502
+ const { clientDiscoveries, authMetadata } = buildAuthServerMetadata(ctx, opts);
503
+ return applyOAuthProviderMetadataExtensions(ctx, opts, "oauth-authorization-server", {
504
+ ...mergeDiscoveryMetadata(clientDiscoveries),
505
+ ...authMetadata
506
+ });
507
+ }
508
+ function oidcServerMetadata(ctx, opts) {
509
+ const baseURL = ctx.context.baseURL;
510
+ const { jwtPluginOptions, clientDiscoveries, authMetadata } = buildAuthServerMetadata(ctx, opts);
511
+ const metadata = {
512
+ ...authMetadata,
513
+ claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
514
+ userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
515
+ subject_types_supported: opts.pairwiseSecret ? ["public", "pairwise"] : ["public"],
516
+ id_token_signing_alg_values_supported: (() => {
517
+ if (opts.disableJwtPlugin) return ["HS256"];
518
+ const primary = jwtPluginOptions?.jwks?.keyPairConfig?.alg ?? "EdDSA";
519
+ const extras = jwtPluginOptions?.jwks?.keyPairConfigs?.map((c) => c.alg) ?? [];
520
+ return Array.from(new Set([primary, ...extras]));
521
+ })(),
522
+ end_session_endpoint: `${baseURL}/oauth2/end-session`,
523
+ acr_values_supported: ["urn:mace:incommon:iap:bronze"],
524
+ prompt_values_supported: [
525
+ "login",
526
+ "consent",
527
+ "create",
528
+ "select_account",
529
+ "none"
530
+ ]
531
+ };
532
+ return applyOAuthProviderMetadataExtensions(ctx, opts, "openid-configuration", {
533
+ ...mergeDiscoveryMetadata(clientDiscoveries),
534
+ ...metadata
535
+ });
536
+ }
537
+ const METADATA_CACHE_CONTROL = "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400";
538
+ function metadataResponse(body, extraHeaders) {
539
+ const headers = new Headers(extraHeaders);
540
+ if (!headers.has("Cache-Control")) headers.set("Cache-Control", METADATA_CACHE_CONTROL);
541
+ headers.set("Content-Type", "application/json");
542
+ return new Response(JSON.stringify(body), {
543
+ status: 200,
544
+ headers
545
+ });
546
+ }
547
+ /**
548
+ * Provides an exportable `/.well-known/oauth-authorization-server`.
549
+ *
550
+ * Useful when basePath prevents the endpoint from being located at the root
551
+ * and must be provided manually.
552
+ *
553
+ * @external
554
+ */
555
+ const oauthProviderAuthServerMetadata = (auth, opts) => {
556
+ return async (request) => {
557
+ return metadataResponse(await auth.api.getOAuthServerConfig({
558
+ request,
559
+ asResponse: false
560
+ }), opts?.headers);
561
+ };
562
+ };
563
+ /**
564
+ * Provides an exportable `/.well-known/openid-configuration`.
565
+ *
566
+ * Useful when basePath prevents the endpoint from being located at the root
567
+ * and must be provided manually.
568
+ *
569
+ * @external
570
+ */
571
+ const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
572
+ return async (request) => {
573
+ return metadataResponse(await auth.api.getOpenIdConfig({
574
+ request,
575
+ asResponse: false
576
+ }), opts?.headers);
577
+ };
578
+ };
579
+ //#endregion
1258
580
  //#region src/oauth-endpoint.ts
1259
581
  /**
1260
582
  * Wraps `createAuthEndpoint` so zod schemas stay the single source of truth
@@ -1387,6 +709,64 @@ async function assertClientPrivileges(ctx, session, opts, action) {
1387
709
  })) throw new APIError("UNAUTHORIZED");
1388
710
  }
1389
711
  //#endregion
712
+ //#region src/utils/initial-access-token.ts
713
+ /**
714
+ * Builds an RFC 6750 §3 Bearer challenge for the registration endpoint. The
715
+ * `error` code maps to the status per RFC 6750 §3.1 (`invalid_request` → 400,
716
+ * `invalid_token` → 401) and is echoed in both the `WWW-Authenticate` challenge
717
+ * and the JSON body.
718
+ *
719
+ * @see https://datatracker.ietf.org/doc/html/rfc6750#section-3
720
+ */
721
+ function registrationBearerError(status, error, errorDescription) {
722
+ return new APIError$1(status, {
723
+ error,
724
+ error_description: errorDescription
725
+ }, {
726
+ "WWW-Authenticate": `Bearer error="${error}"`,
727
+ ...NO_STORE_HEADERS
728
+ });
729
+ }
730
+ /**
731
+ * Resolves an RFC 7591 initial access token from the request and authorizes it
732
+ * against the deployment-supplied validator.
733
+ *
734
+ * Returns the authorization (optionally carrying a `referenceId`) when a valid
735
+ * token is present, or `undefined` when no Bearer credentials were sent so the
736
+ * caller can fall back to session or open registration. Throws an RFC 6750
737
+ * Bearer challenge when the header is malformed, no validator is configured, or
738
+ * the token is rejected.
739
+ *
740
+ * @see https://datatracker.ietf.org/doc/html/rfc7591#section-3
741
+ */
742
+ async function authorizeInitialAccessToken(ctx, opts, clientMetadata) {
743
+ const headers = ctx.headers;
744
+ let initialAccessToken;
745
+ try {
746
+ initialAccessToken = parseBearerToken(headers?.get("authorization"));
747
+ } catch {
748
+ throw registrationBearerError("BAD_REQUEST", "invalid_request", "Malformed initial access token Authorization header");
749
+ }
750
+ if (!initialAccessToken || !headers) return;
751
+ if (!opts.validateInitialAccessToken) throw registrationBearerError("UNAUTHORIZED", "invalid_token", "Invalid initial access token");
752
+ let authorization;
753
+ try {
754
+ authorization = await opts.validateInitialAccessToken({
755
+ initialAccessToken,
756
+ headers,
757
+ clientMetadata
758
+ });
759
+ } catch (error) {
760
+ if (error instanceof APIError$1) throw error;
761
+ throw new APIError$1("INTERNAL_SERVER_ERROR", {
762
+ error: "server_error",
763
+ error_description: "Initial access token validation failed"
764
+ });
765
+ }
766
+ if (!authorization) throw registrationBearerError("UNAUTHORIZED", "invalid_token", "Invalid initial access token");
767
+ return authorization;
768
+ }
769
+ //#endregion
1390
770
  //#region src/register.ts
1391
771
  /**
1392
772
  * Resolves the auth method and type for unauthenticated DCR.
@@ -1403,18 +783,42 @@ function resolveUnauthenticatedAuth(body) {
1403
783
  type: body.type === "web" ? void 0 : body.type
1404
784
  };
1405
785
  }
786
+ const DEFAULT_REGISTRATION_GRANT_TYPES = ["authorization_code"];
787
+ function resolveRegistrationGrantTypes(client) {
788
+ const grantTypes = client.grant_types ?? [...DEFAULT_REGISTRATION_GRANT_TYPES];
789
+ if (grantTypes.length > 0) return grantTypes;
790
+ throw new APIError("BAD_REQUEST", {
791
+ error: "invalid_client_metadata",
792
+ error_description: "grant_types must contain at least one grant type"
793
+ });
794
+ }
795
+ function resolveRegistrationResponseTypes(client, grantTypes) {
796
+ if (client.response_types) return client.response_types;
797
+ return grantTypes.includes("authorization_code") ? ["code"] : void 0;
798
+ }
799
+ function applyOAuthClientRegistrationDefaults(client) {
800
+ const grantTypes = resolveRegistrationGrantTypes(client);
801
+ return {
802
+ ...client,
803
+ token_endpoint_auth_method: client.token_endpoint_auth_method ?? "client_secret_basic",
804
+ grant_types: grantTypes,
805
+ response_types: resolveRegistrationResponseTypes(client, grantTypes)
806
+ };
807
+ }
1406
808
  async function registerEndpoint(ctx, opts) {
809
+ const body = ctx.body;
1407
810
  if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
1408
811
  error: "access_denied",
1409
812
  error_description: "Client registration is disabled"
1410
813
  });
1411
- const body = ctx.body;
1412
814
  const session = await getSessionFromCtx(ctx);
1413
- if (!(session || opts.allowUnauthenticatedClientRegistration)) throw new APIError("UNAUTHORIZED", {
1414
- error: "invalid_token",
1415
- error_description: "Authentication required for client registration"
815
+ const tokenAuthorization = session ? void 0 : await authorizeInitialAccessToken(ctx, opts, body);
816
+ const isTokenAuthorized = Boolean(tokenAuthorization);
817
+ if (!(session || isTokenAuthorized || opts.allowUnauthenticatedClientRegistration)) throw new APIError("UNAUTHORIZED", { error_description: "Authentication required for client registration" }, {
818
+ "WWW-Authenticate": "Bearer",
819
+ ...NO_STORE_HEADERS
1416
820
  });
1417
- if (!session) {
821
+ if (!session && !isTokenAuthorized) {
1418
822
  if (body.grant_types?.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
1419
823
  error: "invalid_client_metadata",
1420
824
  error_description: "client_credentials grant requires authenticated registration"
@@ -1424,47 +828,83 @@ async function registerEndpoint(ctx, opts) {
1424
828
  body.type = resolved.type;
1425
829
  }
1426
830
  if (!body.scope) body.scope = (opts.clientRegistrationDefaultScopes ?? opts.scopes)?.join(" ");
1427
- return createOAuthClientEndpoint(ctx, opts, { isRegister: true });
831
+ const requestedResources = Array.isArray(body.resources) ? [...new Set(body.resources.filter((resource) => typeof resource === "string" && resource.length > 0))] : [];
832
+ if (requestedResources.length > 0) for (const identifier of requestedResources) {
833
+ const row = await getResource(ctx, opts, identifier);
834
+ if (!row) throw new APIError("BAD_REQUEST", {
835
+ error: "invalid_target",
836
+ error_description: `requested resource ${identifier} does not exist`
837
+ });
838
+ if (row.disabled) throw new APIError("BAD_REQUEST", {
839
+ error: "invalid_target",
840
+ error_description: `requested resource ${identifier} is disabled`
841
+ });
842
+ }
843
+ return createOAuthClientEndpoint(ctx, opts, {
844
+ isRegister: true,
845
+ session,
846
+ referenceId: tokenAuthorization?.referenceId,
847
+ resources: requestedResources.length > 0 ? requestedResources : void 0
848
+ });
1428
849
  }
1429
850
  async function checkOAuthClient(client, opts, settings) {
1430
- const isPublic = client.token_endpoint_auth_method === "none";
1431
- if (client.type) {
1432
- if (isPublic && !(client.type === "native" || client.type === "user-agent-based")) throw new APIError("BAD_REQUEST", {
851
+ const clientWithDefaults = applyOAuthClientRegistrationDefaults(client);
852
+ const isPublic = clientWithDefaults.token_endpoint_auth_method === "none";
853
+ const tokenEndpointAuthMethod = clientWithDefaults.token_endpoint_auth_method ?? "client_secret_basic";
854
+ if (!new Set(getSupportedAuthMethods(opts, { includeNone: true })).has(tokenEndpointAuthMethod)) throw new APIError("BAD_REQUEST", {
855
+ error: "invalid_client_metadata",
856
+ error_description: `unsupported token_endpoint_auth_method ${tokenEndpointAuthMethod}`
857
+ });
858
+ if (clientWithDefaults.dpop_bound_access_tokens !== void 0 && typeof clientWithDefaults.dpop_bound_access_tokens !== "boolean") throw new APIError("BAD_REQUEST", {
859
+ error: "invalid_client_metadata",
860
+ error_description: "dpop_bound_access_tokens must be a boolean"
861
+ });
862
+ if (clientWithDefaults.type) {
863
+ if (isPublic && !(clientWithDefaults.type === "native" || clientWithDefaults.type === "user-agent-based")) throw new APIError("BAD_REQUEST", {
1433
864
  error: "invalid_client_metadata",
1434
865
  error_description: `Type must be 'native' or 'user-agent-based' for public applications`
1435
866
  });
1436
- else if (!isPublic && !(client.type === "web")) throw new APIError("BAD_REQUEST", {
867
+ else if (!isPublic && !(clientWithDefaults.type === "web")) throw new APIError("BAD_REQUEST", {
1437
868
  error: "invalid_client_metadata",
1438
869
  error_description: `Type must be 'web' for confidential applications`
1439
870
  });
1440
871
  }
1441
- if ((!client.grant_types || client.grant_types.includes("authorization_code")) && (!client.redirect_uris || client.redirect_uris.length === 0)) throw new APIError("BAD_REQUEST", {
872
+ const grantTypes = clientWithDefaults.grant_types ?? [];
873
+ const responseTypes = clientWithDefaults.response_types;
874
+ if (grantTypes.includes("authorization_code") && (!clientWithDefaults.redirect_uris || clientWithDefaults.redirect_uris.length === 0)) throw new APIError("BAD_REQUEST", {
1442
875
  error: "invalid_redirect_uri",
1443
876
  error_description: "Redirect URIs are required for authorization_code and implicit grant types"
1444
877
  });
1445
- const grantTypes = client.grant_types ?? ["authorization_code"];
1446
- const responseTypes = client.response_types ?? ["code"];
1447
- if (grantTypes.includes("authorization_code") && !responseTypes.includes("code")) throw new APIError("BAD_REQUEST", {
878
+ const supportedGrantTypes = new Set(getSupportedGrantTypes(opts));
879
+ for (const grantType of grantTypes) if (!supportedGrantTypes.has(grantType)) throw new APIError("BAD_REQUEST", {
880
+ error: "invalid_client_metadata",
881
+ error_description: `unsupported grant_type ${grantType}`
882
+ });
883
+ if (grantTypes.includes("authorization_code") && !responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
1448
884
  error: "invalid_client_metadata",
1449
885
  error_description: "When 'authorization_code' grant type is used, 'code' response type must be included"
1450
886
  });
1451
- if (client.subject_type !== void 0) {
1452
- if (client.subject_type !== "public" && client.subject_type !== "pairwise") throw new APIError("BAD_REQUEST", {
887
+ if (!grantTypes.includes("authorization_code") && responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
888
+ error: "invalid_client_metadata",
889
+ error_description: "When 'code' response type is used, 'authorization_code' grant type must be included"
890
+ });
891
+ if (clientWithDefaults.subject_type !== void 0) {
892
+ if (clientWithDefaults.subject_type !== "public" && clientWithDefaults.subject_type !== "pairwise") throw new APIError("BAD_REQUEST", {
1453
893
  error: "invalid_client_metadata",
1454
894
  error_description: `subject_type must be "public" or "pairwise"`
1455
895
  });
1456
- if (client.subject_type === "pairwise" && !opts.pairwiseSecret) throw new APIError("BAD_REQUEST", {
896
+ if (clientWithDefaults.subject_type === "pairwise" && !opts.pairwiseSecret) throw new APIError("BAD_REQUEST", {
1457
897
  error: "invalid_client_metadata",
1458
898
  error_description: "pairwise subject_type requires server pairwiseSecret configuration"
1459
899
  });
1460
- if (client.subject_type === "pairwise" && client.redirect_uris && client.redirect_uris.length > 1) {
1461
- if (new Set(client.redirect_uris.map((uri) => new URL(uri).host)).size > 1) throw new APIError("BAD_REQUEST", {
900
+ if (clientWithDefaults.subject_type === "pairwise" && clientWithDefaults.redirect_uris && clientWithDefaults.redirect_uris.length > 1) {
901
+ if (new Set(clientWithDefaults.redirect_uris.map((uri) => new URL(uri).host)).size > 1) throw new APIError("BAD_REQUEST", {
1462
902
  error: "invalid_client_metadata",
1463
903
  error_description: "pairwise clients with redirect_uris on different hosts require a sector_identifier_uri, which is not yet supported. All redirect_uris must share the same host."
1464
904
  });
1465
905
  }
1466
906
  }
1467
- const requestedScopes = (client?.scope)?.split(" ").filter((v) => v.length);
907
+ const requestedScopes = (clientWithDefaults?.scope)?.split(" ").filter((v) => v.length);
1468
908
  const allowedScopes = settings?.isRegister ? opts.clientRegistrationAllowedScopes ?? opts.scopes : opts.scopes;
1469
909
  if (allowedScopes) {
1470
910
  const validScopes = new Set(allowedScopes);
@@ -1473,21 +913,22 @@ async function checkOAuthClient(client, opts, settings) {
1473
913
  error_description: `cannot request scope ${requestedScope}`
1474
914
  });
1475
915
  }
1476
- if (settings?.isRegister && client.require_pkce === false) throw new APIError("BAD_REQUEST", {
916
+ if (settings?.isRegister && clientWithDefaults.require_pkce === false) throw new APIError("BAD_REQUEST", {
1477
917
  error: "invalid_client_metadata",
1478
918
  error_description: `pkce is required for registered clients.`
1479
919
  });
1480
- if (client.token_endpoint_auth_method === "private_key_jwt") {
1481
- if (client.jwks && client.jwks_uri) throw new APIError("BAD_REQUEST", {
920
+ const usesAssertionKeyMaterial = tokenEndpointAuthMethod === "private_key_jwt" || isExtensionTokenEndpointAuthMethod(opts, tokenEndpointAuthMethod);
921
+ if (clientWithDefaults.jwks || clientWithDefaults.jwks_uri) {
922
+ if (!usesAssertionKeyMaterial) throw new APIError("BAD_REQUEST", {
1482
923
  error: "invalid_client_metadata",
1483
- error_description: "jwks and jwks_uri are mutually exclusive"
924
+ error_description: "jwks and jwks_uri are only allowed with private_key_jwt or an assertion-based authentication method"
1484
925
  });
1485
- if (!client.jwks && !client.jwks_uri) throw new APIError("BAD_REQUEST", {
926
+ if (clientWithDefaults.jwks && clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
1486
927
  error: "invalid_client_metadata",
1487
- error_description: "private_key_jwt requires either jwks or jwks_uri"
928
+ error_description: "jwks and jwks_uri are mutually exclusive"
1488
929
  });
1489
- if (client.jwks_uri) try {
1490
- const uri = new URL(client.jwks_uri);
930
+ if (clientWithDefaults.jwks_uri) try {
931
+ const uri = new URL(clientWithDefaults.jwks_uri);
1491
932
  if (uri.protocol !== "https:") throw new APIError("BAD_REQUEST", {
1492
933
  error: "invalid_client_metadata",
1493
934
  error_description: "jwks_uri must use HTTPS"
@@ -1507,40 +948,73 @@ async function checkOAuthClient(client, opts, settings) {
1507
948
  error_description: "jwks_uri must be a valid URL"
1508
949
  });
1509
950
  }
1510
- if (client.jwks) {
1511
- const keys = Array.isArray(client.jwks) ? client.jwks : client.jwks.keys;
951
+ if (clientWithDefaults.jwks) {
952
+ const keys = Array.isArray(clientWithDefaults.jwks) ? clientWithDefaults.jwks : clientWithDefaults.jwks.keys;
1512
953
  if (!Array.isArray(keys) || keys.length === 0) throw new APIError("BAD_REQUEST", {
1513
954
  error: "invalid_client_metadata",
1514
955
  error_description: "jwks must be a non-empty array of JWK objects or a JWKS document {keys:[...]}"
1515
956
  });
1516
957
  }
1517
- } else if (client.jwks || client.jwks_uri) throw new APIError("BAD_REQUEST", {
958
+ }
959
+ if (tokenEndpointAuthMethod === "private_key_jwt" && !clientWithDefaults.jwks && !clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
1518
960
  error: "invalid_client_metadata",
1519
- error_description: "jwks and jwks_uri are only allowed with private_key_jwt authentication"
961
+ error_description: "private_key_jwt requires either jwks or jwks_uri"
1520
962
  });
963
+ if (clientWithDefaults.backchannel_logout_uri !== void 0) {
964
+ if (opts.disableJwtPlugin) throw new APIError("BAD_REQUEST", {
965
+ error: "invalid_client_metadata",
966
+ error_description: "backchannel_logout_uri requires the jwt plugin (disableJwtPlugin must be false)"
967
+ });
968
+ let url;
969
+ try {
970
+ url = new URL(clientWithDefaults.backchannel_logout_uri);
971
+ } catch {
972
+ throw new APIError("BAD_REQUEST", {
973
+ error: "invalid_client_metadata",
974
+ error_description: "backchannel_logout_uri must be an absolute URL"
975
+ });
976
+ }
977
+ if (url.protocol !== "https:" && url.protocol !== "http:") throw new APIError("BAD_REQUEST", {
978
+ error: "invalid_client_metadata",
979
+ error_description: "backchannel_logout_uri must use http or https"
980
+ });
981
+ if (clientWithDefaults.backchannel_logout_uri.includes("#")) throw new APIError("BAD_REQUEST", {
982
+ error: "invalid_client_metadata",
983
+ error_description: "backchannel_logout_uri must not include a fragment component"
984
+ });
985
+ const loopback = isLoopbackHost(url.hostname);
986
+ if (!isPublic && url.protocol !== "https:" && !loopback) throw new APIError("BAD_REQUEST", {
987
+ error: "invalid_client_metadata",
988
+ error_description: "backchannel_logout_uri must use https for confidential clients"
989
+ });
990
+ if (isPrivateHostname(url.hostname) && !loopback) throw new APIError("BAD_REQUEST", {
991
+ error: "invalid_client_metadata",
992
+ error_description: "backchannel_logout_uri must not point to a private or reserved address"
993
+ });
994
+ }
1521
995
  }
1522
996
  async function createOAuthClientEndpoint(ctx, opts, settings) {
1523
- const body = ctx.body;
1524
- const session = await getSessionFromCtx(ctx);
1525
- if (settings.isRegister) {
1526
- if (session) await assertClientPrivileges(ctx, session, opts, "create");
1527
- } else await assertClientPrivileges(ctx, session, opts, "create");
997
+ const body = applyOAuthClientRegistrationDefaults(ctx.body);
998
+ const session = settings.session !== void 0 ? settings.session : await getSessionFromCtx(ctx);
999
+ if (!settings.isRegister || session) await assertClientPrivileges(ctx, session, opts, "create");
1528
1000
  const isPublic = body.token_endpoint_auth_method === "none";
1529
1001
  const isPrivateKeyJwt = body.token_endpoint_auth_method === "private_key_jwt";
1530
- await checkOAuthClient(ctx.body, opts, {
1002
+ const isExtensionAuthMethod = isExtensionTokenEndpointAuthMethod(opts, body.token_endpoint_auth_method);
1003
+ await checkOAuthClient(body, opts, {
1531
1004
  ...settings,
1532
1005
  ctx
1533
1006
  });
1534
1007
  const clientId = opts.generateClientId?.() || generateRandomString(32, "a-z", "A-Z");
1535
- const clientSecret = isPublic || isPrivateKeyJwt ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
1008
+ const clientSecret = isPublic || isPrivateKeyJwt || isExtensionAuthMethod ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
1536
1009
  const storedClientSecret = clientSecret ? await storeClientSecret(ctx, opts, clientSecret) : void 0;
1537
1010
  const iat = Math.floor(Date.now() / 1e3);
1538
- const referenceId = opts.clientReference ? await opts.clientReference({
1539
- user: session?.user,
1540
- session: session?.session
1541
- }) : void 0;
1011
+ const referenceId = settings.referenceId ?? (session && opts.clientReference ? await opts.clientReference({
1012
+ user: session.user,
1013
+ session: session.session
1014
+ }) : void 0);
1542
1015
  const schema = oauthToSchema({
1543
- ...body ?? {},
1016
+ ...body,
1017
+ redirect_uris: body.redirect_uris ?? [],
1544
1018
  disabled: void 0,
1545
1019
  client_secret_expires_at: storedClientSecret ? settings.isRegister && opts?.clientRegistrationClientSecretExpiration ? toExpJWT(opts.clientRegistrationClientSecretExpiration, iat) : 0 : void 0,
1546
1020
  client_id: clientId,
@@ -1550,24 +1024,38 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1550
1024
  user_id: referenceId ? void 0 : session?.session.userId,
1551
1025
  reference_id: referenceId
1552
1026
  });
1553
- const client = await ctx.context.adapter.create({
1554
- model: "oauthClient",
1555
- data: {
1556
- ...schema,
1557
- createdAt: /* @__PURE__ */ new Date(iat * 1e3),
1558
- updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
1559
- }
1560
- });
1561
- return ctx.json(schemaToOAuth({
1562
- ...client,
1027
+ const resources = settings.resources ?? [];
1028
+ const responseBody = schemaToOAuth({
1029
+ ...await runWithTransaction(ctx.context.adapter, async () => {
1030
+ const createdClient = await ctx.context.adapter.create({
1031
+ model: "oauthClient",
1032
+ data: {
1033
+ ...schema,
1034
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3),
1035
+ updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
1036
+ }
1037
+ });
1038
+ if (resources.length > 0) {
1039
+ const linkModel = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
1040
+ const now = /* @__PURE__ */ new Date();
1041
+ for (const resourceId of resources) await ctx.context.adapter.create({
1042
+ model: linkModel,
1043
+ forceAllowId: true,
1044
+ data: {
1045
+ id: buildClientResourceLinkId(clientId, resourceId),
1046
+ clientId,
1047
+ resourceId,
1048
+ createdAt: now
1049
+ }
1050
+ });
1051
+ }
1052
+ return createdClient;
1053
+ }),
1563
1054
  clientSecret: clientSecret ? (opts.prefix?.clientSecret ?? "") + clientSecret : void 0
1564
- }), {
1565
- status: 201,
1566
- headers: {
1567
- "Cache-Control": "no-store",
1568
- Pragma: "no-cache"
1569
- }
1570
1055
  });
1056
+ if (resources.length > 0) responseBody.resources = resources;
1057
+ ctx.setStatus(201);
1058
+ return ctx.json(responseBody);
1571
1059
  }
1572
1060
  /**
1573
1061
  * Converts an OAuth 2.0 Dynamic Client Schema to a Database Schema
@@ -1576,7 +1064,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1576
1064
  * @returns
1577
1065
  */
1578
1066
  function oauthToSchema(input) {
1579
- const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: inputJwks, jwks_uri: jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1067
+ const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: inputJwks, jwks_uri: jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, backchannel_logout_uri: backchannelLogoutUri, backchannel_logout_session_required: backchannelLogoutSessionRequired, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, dpop_bound_access_tokens: dpopBoundAccessTokens, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1580
1068
  const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
1581
1069
  const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
1582
1070
  const scopes = _scope?.split(" ");
@@ -1604,6 +1092,8 @@ function oauthToSchema(input) {
1604
1092
  softwareStatement,
1605
1093
  redirectUris,
1606
1094
  postLogoutRedirectUris,
1095
+ backchannelLogoutUri,
1096
+ backchannelLogoutSessionRequired,
1607
1097
  tokenEndpointAuthMethod,
1608
1098
  grantTypes,
1609
1099
  responseTypes,
@@ -1614,6 +1104,7 @@ function oauthToSchema(input) {
1614
1104
  skipConsent,
1615
1105
  enableEndSession,
1616
1106
  requirePKCE,
1107
+ dpopBoundAccessTokens,
1617
1108
  subjectType,
1618
1109
  referenceId,
1619
1110
  metadata
@@ -1626,7 +1117,7 @@ function oauthToSchema(input) {
1626
1117
  * @returns
1627
1118
  */
1628
1119
  function schemaToOAuth(input) {
1629
- const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, subjectType, referenceId, metadata } = input;
1120
+ const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, backchannelLogoutUri, backchannelLogoutSessionRequired, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, dpopBoundAccessTokens, subjectType, referenceId, metadata } = input;
1630
1121
  const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
1631
1122
  const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
1632
1123
  const _scopes = scopes?.join(" ");
@@ -1651,6 +1142,8 @@ function schemaToOAuth(input) {
1651
1142
  software_statement: softwareStatement ?? void 0,
1652
1143
  redirect_uris: redirectUris ?? [],
1653
1144
  post_logout_redirect_uris: postLogoutRedirectUris ?? void 0,
1145
+ backchannel_logout_uri: backchannelLogoutUri ?? void 0,
1146
+ backchannel_logout_session_required: backchannelLogoutSessionRequired ?? void 0,
1654
1147
  token_endpoint_auth_method: tokenEndpointAuthMethod ?? void 0,
1655
1148
  grant_types: grantTypes ?? void 0,
1656
1149
  response_types: responseTypes ?? void 0,
@@ -1660,6 +1153,7 @@ function schemaToOAuth(input) {
1660
1153
  skip_consent: skipConsent ?? void 0,
1661
1154
  enable_end_session: enableEndSession ?? void 0,
1662
1155
  require_pkce: requirePKCE ?? void 0,
1156
+ dpop_bound_access_tokens: dpopBoundAccessTokens ?? void 0,
1663
1157
  subject_type: subjectType ?? void 0,
1664
1158
  reference_id: referenceId ?? void 0
1665
1159
  };
@@ -1860,22 +1354,193 @@ async function rotateClientSecretEndpoint(ctx, opts) {
1860
1354
  clientSecret: storedClientSecret,
1861
1355
  updatedAt: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3)
1862
1356
  }
1863
- });
1864
- if (!updatedClient) throw new APIError("INTERNAL_SERVER_ERROR", {
1865
- error_description: "unable to update client",
1866
- error: "invalid_client"
1867
- });
1868
- return schemaToOAuth({
1869
- ...updatedClient,
1870
- clientSecret: (opts.prefix?.clientSecret ?? "") + clientSecret
1871
- });
1872
- }
1873
- //#endregion
1874
- //#region src/oauthClient/index.ts
1875
- const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/create-client", {
1357
+ });
1358
+ if (!updatedClient) throw new APIError("INTERNAL_SERVER_ERROR", {
1359
+ error_description: "unable to update client",
1360
+ error: "invalid_client"
1361
+ });
1362
+ return schemaToOAuth({
1363
+ ...updatedClient,
1364
+ clientSecret: (opts.prefix?.clientSecret ?? "") + clientSecret
1365
+ });
1366
+ }
1367
+ //#endregion
1368
+ //#region src/oauthClient/index.ts
1369
+ const tokenEndpointAuthMethodSchema = z.string().trim().min(1);
1370
+ const grantTypesSchema = z.array(z.string().trim().min(1)).min(1);
1371
+ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/create-client", {
1372
+ method: "POST",
1373
+ body: z.object({
1374
+ redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
1375
+ scope: z.string().optional(),
1376
+ client_name: z.string().optional(),
1377
+ client_uri: z.string().optional(),
1378
+ logo_uri: z.string().optional(),
1379
+ contacts: z.array(z.string().min(1)).min(1).optional(),
1380
+ tos_uri: z.string().optional(),
1381
+ policy_uri: z.string().optional(),
1382
+ software_id: z.string().optional(),
1383
+ software_version: z.string().optional(),
1384
+ software_statement: z.string().optional(),
1385
+ post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
1386
+ backchannel_logout_uri: SafeUrlSchema.optional(),
1387
+ backchannel_logout_session_required: z.boolean().optional(),
1388
+ token_endpoint_auth_method: tokenEndpointAuthMethodSchema.optional(),
1389
+ jwks: z.union([z.array(z.record(z.string(), z.unknown())).min(1), z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) })]).optional(),
1390
+ jwks_uri: z.string().optional(),
1391
+ grant_types: grantTypesSchema.optional(),
1392
+ response_types: z.array(z.enum(["code"])).optional(),
1393
+ type: z.enum([
1394
+ "web",
1395
+ "native",
1396
+ "user-agent-based"
1397
+ ]).optional(),
1398
+ client_secret_expires_at: z.union([z.string(), z.number()]).optional().default(0),
1399
+ skip_consent: z.boolean().optional(),
1400
+ enable_end_session: z.boolean().optional(),
1401
+ require_pkce: z.boolean().optional(),
1402
+ dpop_bound_access_tokens: z.boolean().optional(),
1403
+ subject_type: z.enum(["public", "pairwise"]).optional(),
1404
+ metadata: z.record(z.string(), z.unknown()).optional()
1405
+ }),
1406
+ metadata: {
1407
+ noStore: true,
1408
+ SERVER_ONLY: true,
1409
+ openapi: {
1410
+ description: "Register an OAuth2 application",
1411
+ responses: { "201": {
1412
+ description: "OAuth2 application registered successfully",
1413
+ content: { "application/json": { schema: {
1414
+ type: "object",
1415
+ properties: {
1416
+ client_id: {
1417
+ type: "string",
1418
+ description: "Unique identifier for the client"
1419
+ },
1420
+ client_secret: {
1421
+ type: "string",
1422
+ description: "Secret key for the client"
1423
+ },
1424
+ client_secret_expires_at: {
1425
+ type: "number",
1426
+ description: "Time the client secret will expire. If 0, the client secret will never expire."
1427
+ },
1428
+ scope: {
1429
+ type: "string",
1430
+ description: "Space-separated scopes allowed by the client"
1431
+ },
1432
+ user_id: {
1433
+ type: "string",
1434
+ description: "ID of the user who registered the client, null if registered anonymously"
1435
+ },
1436
+ client_id_issued_at: {
1437
+ type: "number",
1438
+ description: "Creation timestamp of this client"
1439
+ },
1440
+ client_name: {
1441
+ type: "string",
1442
+ description: "Name of the OAuth2 application"
1443
+ },
1444
+ client_uri: {
1445
+ type: "string",
1446
+ description: "URI of the OAuth2 application"
1447
+ },
1448
+ logo_uri: {
1449
+ type: "string",
1450
+ description: "Icon URI for the application"
1451
+ },
1452
+ contacts: {
1453
+ type: "array",
1454
+ items: { type: "string" },
1455
+ description: "List representing ways to contact people responsible for this client, typically email addresses"
1456
+ },
1457
+ tos_uri: {
1458
+ type: "string",
1459
+ description: "Client's terms of service uri"
1460
+ },
1461
+ policy_uri: {
1462
+ type: "string",
1463
+ description: "Client's policy uri"
1464
+ },
1465
+ software_id: {
1466
+ type: "string",
1467
+ description: "Unique identifier assigned by the developer to help in the dynamic registration process"
1468
+ },
1469
+ software_version: {
1470
+ type: "string",
1471
+ description: "Version identifier for the software_id"
1472
+ },
1473
+ software_statement: {
1474
+ type: "string",
1475
+ description: "JWT containing metadata values about the client software as claims"
1476
+ },
1477
+ redirect_uris: {
1478
+ type: "array",
1479
+ items: {
1480
+ type: "string",
1481
+ format: "uri"
1482
+ },
1483
+ description: "List of allowed redirect uris"
1484
+ },
1485
+ token_endpoint_auth_method: {
1486
+ type: "string",
1487
+ description: "Requested authentication method for the token endpoint"
1488
+ },
1489
+ grant_types: {
1490
+ type: "array",
1491
+ items: { type: "string" },
1492
+ description: "Grant types the client may use at the token endpoint"
1493
+ },
1494
+ response_types: {
1495
+ type: "array",
1496
+ items: {
1497
+ type: "string",
1498
+ enum: ["code"]
1499
+ },
1500
+ description: "Response types the client may use at the authorization endpoint"
1501
+ },
1502
+ public: {
1503
+ type: "boolean",
1504
+ description: "Whether the client is public as determined by the type"
1505
+ },
1506
+ type: {
1507
+ type: "string",
1508
+ description: "Type of the client",
1509
+ enum: [
1510
+ "web",
1511
+ "native",
1512
+ "user-agent-based"
1513
+ ]
1514
+ },
1515
+ disabled: {
1516
+ type: "boolean",
1517
+ description: "Whether the client is disabled"
1518
+ },
1519
+ require_pkce: {
1520
+ type: "boolean",
1521
+ description: "Whether the client requires PKCE",
1522
+ default: true
1523
+ },
1524
+ metadata: {
1525
+ type: "object",
1526
+ additionalProperties: true,
1527
+ nullable: true,
1528
+ description: "Additional metadata for the application"
1529
+ }
1530
+ },
1531
+ required: ["client_id"]
1532
+ } } }
1533
+ } }
1534
+ }
1535
+ }
1536
+ }, async (ctx) => {
1537
+ return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
1538
+ });
1539
+ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client", {
1876
1540
  method: "POST",
1541
+ use: [sessionMiddleware],
1877
1542
  body: z.object({
1878
- redirect_uris: z.array(SafeUrlSchema).min(1),
1543
+ redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
1879
1544
  scope: z.string().optional(),
1880
1545
  client_name: z.string().optional(),
1881
1546
  client_uri: z.string().optional(),
@@ -1887,37 +1552,25 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
1887
1552
  software_version: z.string().optional(),
1888
1553
  software_statement: z.string().optional(),
1889
1554
  post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
1890
- token_endpoint_auth_method: z.enum([
1891
- "none",
1892
- "client_secret_basic",
1893
- "client_secret_post",
1894
- "private_key_jwt"
1895
- ]).default("client_secret_basic").optional(),
1555
+ backchannel_logout_uri: SafeUrlSchema.optional(),
1556
+ backchannel_logout_session_required: z.boolean().optional(),
1557
+ token_endpoint_auth_method: tokenEndpointAuthMethodSchema.optional(),
1896
1558
  jwks: z.union([z.array(z.record(z.string(), z.unknown())).min(1), z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) })]).optional(),
1897
1559
  jwks_uri: z.string().optional(),
1898
- grant_types: z.array(z.enum([
1899
- "authorization_code",
1900
- "client_credentials",
1901
- "refresh_token"
1902
- ])).default(["authorization_code"]).optional(),
1903
- response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
1560
+ grant_types: grantTypesSchema.optional(),
1561
+ response_types: z.array(z.enum(["code"])).optional(),
1904
1562
  type: z.enum([
1905
1563
  "web",
1906
1564
  "native",
1907
1565
  "user-agent-based"
1908
1566
  ]).optional(),
1909
- client_secret_expires_at: z.union([z.string(), z.number()]).optional().default(0),
1910
- skip_consent: z.boolean().optional(),
1911
- enable_end_session: z.boolean().optional(),
1912
- require_pkce: z.boolean().optional(),
1913
- subject_type: z.enum(["public", "pairwise"]).optional(),
1914
- metadata: z.record(z.string(), z.unknown()).optional()
1567
+ dpop_bound_access_tokens: z.boolean().optional()
1915
1568
  }),
1916
1569
  metadata: {
1917
- SERVER_ONLY: true,
1570
+ noStore: true,
1918
1571
  openapi: {
1919
1572
  description: "Register an OAuth2 application",
1920
- responses: { "200": {
1573
+ responses: { "201": {
1921
1574
  description: "OAuth2 application registered successfully",
1922
1575
  content: { "application/json": { schema: {
1923
1576
  type: "object",
@@ -1993,24 +1646,12 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
1993
1646
  },
1994
1647
  token_endpoint_auth_method: {
1995
1648
  type: "string",
1996
- description: "Requested authentication method for the token endpoint",
1997
- enum: [
1998
- "none",
1999
- "client_secret_basic",
2000
- "client_secret_post"
2001
- ]
1649
+ description: "Requested authentication method for the token endpoint"
2002
1650
  },
2003
1651
  grant_types: {
2004
1652
  type: "array",
2005
- items: {
2006
- type: "string",
2007
- enum: [
2008
- "authorization_code",
2009
- "client_credentials",
2010
- "refresh_token"
2011
- ]
2012
- },
2013
- description: "Requested authentication method for the token endpoint"
1653
+ items: { type: "string" },
1654
+ description: "Grant types the client may use at the token endpoint"
2014
1655
  },
2015
1656
  response_types: {
2016
1657
  type: "array",
@@ -2018,7 +1659,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
2018
1659
  type: "string",
2019
1660
  enum: ["code"]
2020
1661
  },
2021
- description: "Requested authentication method for the token endpoint"
1662
+ description: "Response types the client may use at the authorization endpoint"
2022
1663
  },
2023
1664
  public: {
2024
1665
  type: "boolean",
@@ -2037,11 +1678,6 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
2037
1678
  type: "boolean",
2038
1679
  description: "Whether the client is disabled"
2039
1680
  },
2040
- require_pkce: {
2041
- type: "boolean",
2042
- description: "Whether the client requires PKCE",
2043
- default: true
2044
- },
2045
1681
  metadata: {
2046
1682
  type: "object",
2047
1683
  additionalProperties: true,
@@ -2057,178 +1693,6 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
2057
1693
  }, async (ctx) => {
2058
1694
  return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
2059
1695
  });
2060
- const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client", {
2061
- method: "POST",
2062
- use: [sessionMiddleware],
2063
- body: z.object({
2064
- redirect_uris: z.array(SafeUrlSchema).min(1),
2065
- scope: z.string().optional(),
2066
- client_name: z.string().optional(),
2067
- client_uri: z.string().optional(),
2068
- logo_uri: z.string().optional(),
2069
- contacts: z.array(z.string().min(1)).min(1).optional(),
2070
- tos_uri: z.string().optional(),
2071
- policy_uri: z.string().optional(),
2072
- software_id: z.string().optional(),
2073
- software_version: z.string().optional(),
2074
- software_statement: z.string().optional(),
2075
- post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2076
- token_endpoint_auth_method: z.enum([
2077
- "none",
2078
- "client_secret_basic",
2079
- "client_secret_post",
2080
- "private_key_jwt"
2081
- ]).default("client_secret_basic").optional(),
2082
- jwks: z.union([z.array(z.record(z.string(), z.unknown())).min(1), z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) })]).optional(),
2083
- jwks_uri: z.string().optional(),
2084
- grant_types: z.array(z.enum([
2085
- "authorization_code",
2086
- "client_credentials",
2087
- "refresh_token"
2088
- ])).default(["authorization_code"]).optional(),
2089
- response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
2090
- type: z.enum([
2091
- "web",
2092
- "native",
2093
- "user-agent-based"
2094
- ]).optional()
2095
- }),
2096
- metadata: { openapi: {
2097
- description: "Register an OAuth2 application",
2098
- responses: { "200": {
2099
- description: "OAuth2 application registered successfully",
2100
- content: { "application/json": { schema: {
2101
- type: "object",
2102
- properties: {
2103
- client_id: {
2104
- type: "string",
2105
- description: "Unique identifier for the client"
2106
- },
2107
- client_secret: {
2108
- type: "string",
2109
- description: "Secret key for the client"
2110
- },
2111
- client_secret_expires_at: {
2112
- type: "number",
2113
- description: "Time the client secret will expire. If 0, the client secret will never expire."
2114
- },
2115
- scope: {
2116
- type: "string",
2117
- description: "Space-separated scopes allowed by the client"
2118
- },
2119
- user_id: {
2120
- type: "string",
2121
- description: "ID of the user who registered the client, null if registered anonymously"
2122
- },
2123
- client_id_issued_at: {
2124
- type: "number",
2125
- description: "Creation timestamp of this client"
2126
- },
2127
- client_name: {
2128
- type: "string",
2129
- description: "Name of the OAuth2 application"
2130
- },
2131
- client_uri: {
2132
- type: "string",
2133
- description: "URI of the OAuth2 application"
2134
- },
2135
- logo_uri: {
2136
- type: "string",
2137
- description: "Icon URI for the application"
2138
- },
2139
- contacts: {
2140
- type: "array",
2141
- items: { type: "string" },
2142
- description: "List representing ways to contact people responsible for this client, typically email addresses"
2143
- },
2144
- tos_uri: {
2145
- type: "string",
2146
- description: "Client's terms of service uri"
2147
- },
2148
- policy_uri: {
2149
- type: "string",
2150
- description: "Client's policy uri"
2151
- },
2152
- software_id: {
2153
- type: "string",
2154
- description: "Unique identifier assigned by the developer to help in the dynamic registration process"
2155
- },
2156
- software_version: {
2157
- type: "string",
2158
- description: "Version identifier for the software_id"
2159
- },
2160
- software_statement: {
2161
- type: "string",
2162
- description: "JWT containing metadata values about the client software as claims"
2163
- },
2164
- redirect_uris: {
2165
- type: "array",
2166
- items: {
2167
- type: "string",
2168
- format: "uri"
2169
- },
2170
- description: "List of allowed redirect uris"
2171
- },
2172
- token_endpoint_auth_method: {
2173
- type: "string",
2174
- description: "Response types the client may use",
2175
- enum: [
2176
- "none",
2177
- "client_secret_basic",
2178
- "client_secret_post"
2179
- ]
2180
- },
2181
- grant_types: {
2182
- type: "array",
2183
- items: {
2184
- type: "string",
2185
- enum: [
2186
- "authorization_code",
2187
- "client_credentials",
2188
- "refresh_token"
2189
- ]
2190
- },
2191
- description: "Requested authentication method for the token endpoint"
2192
- },
2193
- response_types: {
2194
- type: "array",
2195
- items: {
2196
- type: "string",
2197
- enum: ["code"]
2198
- },
2199
- description: "Requested authentication method for the token endpoint"
2200
- },
2201
- public: {
2202
- type: "boolean",
2203
- description: "Whether the client is public as determined by the type"
2204
- },
2205
- type: {
2206
- type: "string",
2207
- description: "Type of the client",
2208
- enum: [
2209
- "web",
2210
- "native",
2211
- "user-agent-based"
2212
- ]
2213
- },
2214
- disabled: {
2215
- type: "boolean",
2216
- description: "Whether the client is disabled"
2217
- },
2218
- metadata: {
2219
- type: "object",
2220
- additionalProperties: true,
2221
- nullable: true,
2222
- description: "Additional metadata for the application"
2223
- }
2224
- },
2225
- required: ["client_id"]
2226
- } } }
2227
- } }
2228
- } }
2229
- }, async (ctx) => {
2230
- return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
2231
- });
2232
1696
  const getOAuthClient = (opts) => createAuthEndpoint("/oauth2/get-client", {
2233
1697
  method: "GET",
2234
1698
  use: [sessionMiddleware],
@@ -2282,11 +1746,9 @@ const adminUpdateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/updat
2282
1746
  software_version: z.string().optional(),
2283
1747
  software_statement: z.string().optional(),
2284
1748
  post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2285
- grant_types: z.array(z.enum([
2286
- "authorization_code",
2287
- "client_credentials",
2288
- "refresh_token"
2289
- ])).optional(),
1749
+ backchannel_logout_uri: SafeUrlSchema.optional(),
1750
+ backchannel_logout_session_required: z.boolean().optional(),
1751
+ grant_types: grantTypesSchema.optional(),
2290
1752
  response_types: z.array(z.enum(["code"])).optional(),
2291
1753
  type: z.enum([
2292
1754
  "web",
@@ -2296,6 +1758,7 @@ const adminUpdateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/updat
2296
1758
  client_secret_expires_at: z.union([z.string(), z.number()]).optional(),
2297
1759
  skip_consent: z.boolean().optional(),
2298
1760
  enable_end_session: z.boolean().optional(),
1761
+ dpop_bound_access_tokens: z.boolean().optional(),
2299
1762
  metadata: z.record(z.string(), z.unknown()).optional()
2300
1763
  })
2301
1764
  }),
@@ -2324,11 +1787,9 @@ const updateOAuthClient = (opts) => createAuthEndpoint("/oauth2/update-client",
2324
1787
  software_version: z.string().optional(),
2325
1788
  software_statement: z.string().optional(),
2326
1789
  post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2327
- grant_types: z.array(z.enum([
2328
- "authorization_code",
2329
- "client_credentials",
2330
- "refresh_token"
2331
- ])).optional(),
1790
+ backchannel_logout_uri: SafeUrlSchema.optional(),
1791
+ backchannel_logout_session_required: z.boolean().optional(),
1792
+ grant_types: grantTypesSchema.optional(),
2332
1793
  response_types: z.array(z.enum(["code"])).optional(),
2333
1794
  type: z.enum([
2334
1795
  "web",
@@ -2345,7 +1806,10 @@ const rotateClientSecret = (opts) => createAuthEndpoint("/oauth2/client/rotate-s
2345
1806
  method: "POST",
2346
1807
  use: [sessionMiddleware],
2347
1808
  body: z.object({ client_id: z.string() }),
2348
- metadata: { openapi: { description: "Rotates a confidential client's secret" } }
1809
+ metadata: {
1810
+ noStore: true,
1811
+ openapi: { description: "Rotates a confidential client's secret" }
1812
+ }
2349
1813
  }, async (ctx) => {
2350
1814
  return rotateClientSecretEndpoint(ctx, opts);
2351
1815
  });
@@ -2484,14 +1948,341 @@ const updateOAuthConsent = (opts) => createAuthEndpoint("/oauth2/update-consent"
2484
1948
  }, async (ctx) => {
2485
1949
  return updateConsentEndpoint(ctx, opts);
2486
1950
  });
2487
- const deleteOAuthConsent = (opts) => createAuthEndpoint("/oauth2/delete-consent", {
1951
+ const deleteOAuthConsent = (opts) => createAuthEndpoint("/oauth2/delete-consent", {
1952
+ method: "POST",
1953
+ use: [sessionMiddleware],
1954
+ body: z.object({ id: z.string() }),
1955
+ metadata: { openapi: { description: "Deletes consent granted to a client" } }
1956
+ }, async (ctx) => {
1957
+ return deleteConsentEndpoint(ctx, opts);
1958
+ });
1959
+ //#endregion
1960
+ //#region src/oauthResource/endpoints.ts
1961
+ /**
1962
+ * Gate every admin resource endpoint. Mirrors `assertClientPrivileges`:
1963
+ * a missing session → 401; a defined `resourcePrivileges` callback that
1964
+ * returns falsy → 401 with the original action context preserved.
1965
+ *
1966
+ * When `resourcePrivileges` is undefined, the gate degrades to "any
1967
+ * authenticated session can manage resources" — same forgiving default
1968
+ * as `clientPrivileges`. Operators who care about RBAC must define the
1969
+ * callback.
1970
+ *
1971
+ * @internal
1972
+ */
1973
+ async function assertResourcePrivileges(ctx, session, opts, action, resourceId) {
1974
+ if (!session) throw new APIError("UNAUTHORIZED");
1975
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1976
+ if (!opts.resourcePrivileges) return;
1977
+ if (!await opts.resourcePrivileges({
1978
+ headers: ctx.headers,
1979
+ action,
1980
+ session: session.session,
1981
+ user: session.user,
1982
+ resourceId
1983
+ })) throw new APIError("UNAUTHORIZED");
1984
+ }
1985
+ const resourceModel = (opts) => opts.schema?.oauthResource?.modelName ?? "oauthResource";
1986
+ const linkModel = (opts) => opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
1987
+ const clientModel = (opts) => opts.schema?.oauthClient?.modelName ?? "oauthClient";
1988
+ /**
1989
+ * Decode a URL path-segment parameter.
1990
+ *
1991
+ * better-call's router (better-call@1.3.5) does NOT decode path params —
1992
+ * `tryDecode` is wired into cookie parsing only. So a raw HTTP caller hitting
1993
+ * `/admin/oauth2/resources/https%3A%2F%2Fapi.example.com` lands here with
1994
+ * `ctx.params.identifier === "https%3A%2F%2Fapi.example.com"`, which never
1995
+ * matches the stored `https://api.example.com` row. Decode every path
1996
+ * segment that holds a URI-valued identifier so the admin handlers behave
1997
+ * identically to in-process `auth.api.*` calls (which pass already-decoded
1998
+ * JS strings).
1999
+ *
2000
+ * Falls back to the raw string when decode fails so a malformed identifier
2001
+ * surfaces as a clean NOT_FOUND from the row lookup rather than a 500.
2002
+ *
2003
+ * @internal
2004
+ */
2005
+ function decodePathParam(value) {
2006
+ try {
2007
+ return decodeURIComponent(value);
2008
+ } catch {
2009
+ return value;
2010
+ }
2011
+ }
2012
+ /**
2013
+ * Builds the create-payload from a normalized input. Fills in defaults
2014
+ * for the fields seedResources uses so the admin-CRUD and seed paths
2015
+ * write identical rows.
2016
+ *
2017
+ * @internal
2018
+ */
2019
+ function buildResourceRow(input, now) {
2020
+ return {
2021
+ identifier: input.identifier,
2022
+ name: input.name ?? input.identifier,
2023
+ accessTokenTtl: input.accessTokenTtl ?? null,
2024
+ refreshTokenTtl: input.refreshTokenTtl ?? null,
2025
+ signingAlgorithm: input.signingAlgorithm ?? null,
2026
+ signingKeyId: input.signingKeyId ?? null,
2027
+ allowedScopes: input.allowedScopes ?? null,
2028
+ customClaims: input.customClaims ?? null,
2029
+ dpopBoundAccessTokensRequired: input.dpopBoundAccessTokensRequired ?? false,
2030
+ disabled: input.disabled ?? false,
2031
+ policyVersion: 1,
2032
+ metadata: input.metadata ?? null,
2033
+ createdAt: now,
2034
+ updatedAt: now
2035
+ };
2036
+ }
2037
+ async function createResourceEndpoint(ctx, opts) {
2038
+ await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
2039
+ const input = ctx.body;
2040
+ if (!input?.identifier) throw new APIError("BAD_REQUEST", {
2041
+ error: "invalid_request",
2042
+ error_description: "identifier is required"
2043
+ });
2044
+ await assertIdentifierValid(opts, input.identifier);
2045
+ const now = /* @__PURE__ */ new Date();
2046
+ let created;
2047
+ try {
2048
+ created = await ctx.context.adapter.create({
2049
+ model: resourceModel(opts),
2050
+ data: buildResourceRow(input, now)
2051
+ });
2052
+ } catch (err) {
2053
+ const message = err instanceof Error ? err.message : String(err);
2054
+ if (/unique|duplicate|UNIQUE/i.test(message)) throw new APIError("BAD_REQUEST", {
2055
+ error: "invalid_request",
2056
+ error_description: `resource ${input.identifier} already exists`
2057
+ });
2058
+ throw err;
2059
+ }
2060
+ if (!created) throw new APIError("BAD_REQUEST", {
2061
+ error: "invalid_request",
2062
+ error_description: `resource ${input.identifier} could not be created`
2063
+ });
2064
+ invalidateResourceCache(created.identifier);
2065
+ return ctx.json(created, { status: 201 });
2066
+ }
2067
+ async function listResourcesEndpoint(ctx, opts) {
2068
+ await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "list");
2069
+ const rows = await ctx.context.adapter.findMany({ model: resourceModel(opts) });
2070
+ return ctx.json(rows ?? []);
2071
+ }
2072
+ async function getResourceByIdentifierEndpoint(ctx, opts) {
2073
+ const identifier = decodePathParam(ctx.params.identifier);
2074
+ await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "read", identifier);
2075
+ const row = await ctx.context.adapter.findOne({
2076
+ model: resourceModel(opts),
2077
+ where: [{
2078
+ field: "identifier",
2079
+ value: identifier
2080
+ }]
2081
+ });
2082
+ if (!row) throw new APIError("NOT_FOUND", {
2083
+ error: "not_found",
2084
+ error_description: `resource ${identifier} not found`
2085
+ });
2086
+ return ctx.json(row);
2087
+ }
2088
+ async function updateResourceEndpoint(ctx, opts) {
2089
+ const identifier = decodePathParam(ctx.params.identifier);
2090
+ await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "update", identifier);
2091
+ if (!await ctx.context.adapter.findOne({
2092
+ model: resourceModel(opts),
2093
+ where: [{
2094
+ field: "identifier",
2095
+ value: identifier
2096
+ }]
2097
+ })) throw new APIError("NOT_FOUND", {
2098
+ error: "not_found",
2099
+ error_description: `resource ${identifier} not found`
2100
+ });
2101
+ const update = { updatedAt: /* @__PURE__ */ new Date() };
2102
+ for (const key of [
2103
+ "name",
2104
+ "accessTokenTtl",
2105
+ "refreshTokenTtl",
2106
+ "signingAlgorithm",
2107
+ "signingKeyId",
2108
+ "allowedScopes",
2109
+ "customClaims",
2110
+ "dpopBoundAccessTokensRequired",
2111
+ "disabled",
2112
+ "metadata"
2113
+ ]) if (Object.prototype.hasOwnProperty.call(ctx.body, key)) update[key] = ctx.body[key];
2114
+ await ctx.context.adapter.update({
2115
+ model: resourceModel(opts),
2116
+ where: [{
2117
+ field: "identifier",
2118
+ value: identifier
2119
+ }],
2120
+ update
2121
+ });
2122
+ invalidateResourceCache(identifier);
2123
+ const refreshed = await ctx.context.adapter.findOne({
2124
+ model: resourceModel(opts),
2125
+ where: [{
2126
+ field: "identifier",
2127
+ value: identifier
2128
+ }]
2129
+ });
2130
+ if (!refreshed) throw new APIError("NOT_FOUND", {
2131
+ error: "not_found",
2132
+ error_description: `resource ${identifier} not found`
2133
+ });
2134
+ return ctx.json(refreshed);
2135
+ }
2136
+ async function deleteResourceEndpoint(ctx, opts) {
2137
+ const identifier = decodePathParam(ctx.params.identifier);
2138
+ await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "delete", identifier);
2139
+ if (!await ctx.context.adapter.findOne({
2140
+ model: resourceModel(opts),
2141
+ where: [{
2142
+ field: "identifier",
2143
+ value: identifier
2144
+ }]
2145
+ })) throw new APIError("NOT_FOUND", {
2146
+ error: "not_found",
2147
+ error_description: `resource ${identifier} not found`
2148
+ });
2149
+ await ctx.context.adapter.delete({
2150
+ model: resourceModel(opts),
2151
+ where: [{
2152
+ field: "identifier",
2153
+ value: identifier
2154
+ }]
2155
+ });
2156
+ invalidateResourceCache(identifier);
2157
+ return ctx.json({ deleted: true });
2158
+ }
2159
+ /**
2160
+ * Link a client to a resource.
2161
+ *
2162
+ * Route: `POST /admin/oauth2/resources/:identifier/clients/:client_id`.
2163
+ * Path params carry the identifiers (RESTful linkage) — no body required.
2164
+ * Used by admins when {@link OAuthOptions.enforcePerClientResources} is on.
2165
+ */
2166
+ async function linkClientResourceEndpoint(ctx, opts) {
2167
+ const session = await getSessionFromCtx(ctx);
2168
+ const resourceId = decodePathParam(ctx.params.identifier);
2169
+ const clientId = decodePathParam(ctx.params.client_id);
2170
+ await assertResourcePrivileges(ctx, session, opts, "link", resourceId);
2171
+ if (!await ctx.context.adapter.findOne({
2172
+ model: resourceModel(opts),
2173
+ where: [{
2174
+ field: "identifier",
2175
+ value: resourceId
2176
+ }]
2177
+ })) throw new APIError("NOT_FOUND", {
2178
+ error: "not_found",
2179
+ error_description: `resource ${resourceId} not found`
2180
+ });
2181
+ if (!await ctx.context.adapter.findOne({
2182
+ model: clientModel(opts),
2183
+ where: [{
2184
+ field: "clientId",
2185
+ value: clientId
2186
+ }]
2187
+ })) throw new APIError("NOT_FOUND", {
2188
+ error: "not_found",
2189
+ error_description: `client ${clientId} not found`
2190
+ });
2191
+ const id = buildClientResourceLinkId(clientId, resourceId);
2192
+ try {
2193
+ await ctx.context.adapter.create({
2194
+ model: linkModel(opts),
2195
+ forceAllowId: true,
2196
+ data: {
2197
+ id,
2198
+ clientId,
2199
+ resourceId,
2200
+ createdAt: /* @__PURE__ */ new Date()
2201
+ }
2202
+ });
2203
+ } catch (err) {
2204
+ const message = err instanceof Error ? err.message : String(err);
2205
+ if (/unique|duplicate|UNIQUE/i.test(message)) return ctx.json({
2206
+ linked: true,
2207
+ alreadyLinked: true
2208
+ });
2209
+ throw err;
2210
+ }
2211
+ return ctx.json({ linked: true });
2212
+ }
2213
+ /**
2214
+ * Unlink a client from a resource.
2215
+ *
2216
+ * Route: `DELETE /admin/oauth2/resources/:identifier/clients/:client_id`.
2217
+ * Path params carry the identifiers (RESTful linkage) — no body required.
2218
+ */
2219
+ async function unlinkClientResourceEndpoint(ctx, opts) {
2220
+ const session = await getSessionFromCtx(ctx);
2221
+ const resourceId = decodePathParam(ctx.params.identifier);
2222
+ const clientId = decodePathParam(ctx.params.client_id);
2223
+ await assertResourcePrivileges(ctx, session, opts, "unlink", resourceId);
2224
+ await ctx.context.adapter.deleteMany({
2225
+ model: linkModel(opts),
2226
+ where: [{
2227
+ field: "clientId",
2228
+ value: clientId
2229
+ }, {
2230
+ field: "resourceId",
2231
+ value: resourceId
2232
+ }]
2233
+ });
2234
+ return ctx.json({ unlinked: true });
2235
+ }
2236
+ //#endregion
2237
+ //#region src/oauthResource/index.ts
2238
+ /**
2239
+ * Shared body schema for create/update — every field is optional except
2240
+ * `identifier` on create (validated in the handler). Update accepts a
2241
+ * subset; the handler only writes fields that are explicitly present.
2242
+ */
2243
+ const resourceBodySchema = z.object({
2244
+ identifier: z.string().min(1).optional(),
2245
+ name: z.string().optional(),
2246
+ accessTokenTtl: z.number().int().positive().nullable().optional(),
2247
+ refreshTokenTtl: z.number().int().positive().nullable().optional(),
2248
+ signingAlgorithm: z.enum(JWS_ALGORITHMS).nullable().optional(),
2249
+ signingKeyId: z.string().nullable().optional(),
2250
+ allowedScopes: z.array(z.string()).nullable().optional(),
2251
+ customClaims: z.record(z.string(), z.unknown()).nullable().optional(),
2252
+ dpopBoundAccessTokensRequired: z.boolean().optional(),
2253
+ disabled: z.boolean().optional(),
2254
+ metadata: z.record(z.string(), z.unknown()).nullable().optional()
2255
+ });
2256
+ const adminCreateOAuthResource = (opts) => createAuthEndpoint("/admin/oauth2/resources", {
2488
2257
  method: "POST",
2489
- use: [sessionMiddleware],
2490
- body: z.object({ id: z.string() }),
2491
- metadata: { openapi: { description: "Deletes consent granted to a client" } }
2492
- }, async (ctx) => {
2493
- return deleteConsentEndpoint(ctx, opts);
2494
- });
2258
+ body: resourceBodySchema.required({ identifier: true }),
2259
+ metadata: { SERVER_ONLY: true }
2260
+ }, async (ctx) => createResourceEndpoint(ctx, opts));
2261
+ const adminListOAuthResources = (opts) => createAuthEndpoint("/admin/oauth2/resources", {
2262
+ method: "GET",
2263
+ metadata: { SERVER_ONLY: true }
2264
+ }, async (ctx) => listResourcesEndpoint(ctx, opts));
2265
+ const adminGetOAuthResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier", {
2266
+ method: "GET",
2267
+ metadata: { SERVER_ONLY: true }
2268
+ }, async (ctx) => getResourceByIdentifierEndpoint(ctx, opts));
2269
+ const adminUpdateOAuthResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier", {
2270
+ method: "PATCH",
2271
+ body: resourceBodySchema,
2272
+ metadata: { SERVER_ONLY: true }
2273
+ }, async (ctx) => updateResourceEndpoint(ctx, opts));
2274
+ const adminDeleteOAuthResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier", {
2275
+ method: "DELETE",
2276
+ metadata: { SERVER_ONLY: true }
2277
+ }, async (ctx) => deleteResourceEndpoint(ctx, opts));
2278
+ const adminLinkClientResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier/clients/:client_id", {
2279
+ method: "POST",
2280
+ metadata: { SERVER_ONLY: true }
2281
+ }, async (ctx) => linkClientResourceEndpoint(ctx, opts));
2282
+ const adminUnlinkClientResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier/clients/:client_id", {
2283
+ method: "DELETE",
2284
+ metadata: { SERVER_ONLY: true }
2285
+ }, async (ctx) => unlinkClientResourceEndpoint(ctx, opts));
2495
2286
  //#endregion
2496
2287
  //#region src/revoke.ts
2497
2288
  /**
@@ -2503,21 +2294,28 @@ const deleteOAuthConsent = (opts) => createAuthEndpoint("/oauth2/delete-consent"
2503
2294
  */
2504
2295
  /**
2505
2296
  * Revokes a JWT access token against the configured JWKs.
2506
- * (does nothing if successful since a JWT is not stored on the server)
2297
+ *
2298
+ * A JWT access token is self-contained and never stored, so there is nothing to
2299
+ * delete. Once the token is confirmed to be a valid JWT for this server, the
2300
+ * endpoint reports `unsupported_token_type` (RFC 7009 §2.2.1) instead of a
2301
+ * silent success, so callers can tell that no server-side revocation happened.
2302
+ * An expired JWT or a JWT with an audience rejected by the OAuth resource model
2303
+ * is already inactive and still resolves as a successful no-op. Session-bound
2304
+ * tokens (carrying `sid`) are cut off early by the session-liveness check in
2305
+ * introspection and userinfo.
2507
2306
  */
2508
2307
  async function revokeJwtAccessToken(ctx, opts, token) {
2509
2308
  const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
2510
2309
  const jwtPluginOptions = jwtPlugin?.options;
2511
2310
  try {
2512
- await verifyJwsAccessToken(token, {
2311
+ const verified = await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
2513
2312
  jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
2514
2313
  return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
2515
2314
  },
2516
- verifyOptions: {
2517
- audience: opts.validAudiences ?? ctx.context.baseURL,
2518
- issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL
2519
- }
2520
- });
2315
+ jwksCacheKey: jwtPlugin
2316
+ })), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL });
2317
+ const userInfoAudience = `${ctx.context.baseURL}/oauth2/userinfo`;
2318
+ if (!verified.payload.azp || !await isAudienceClaimAllowed(ctx, opts, verified.payload.aud, [userInfoAudience])) return null;
2521
2319
  } catch (error) {
2522
2320
  if (error instanceof Error) {
2523
2321
  if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
@@ -2530,6 +2328,10 @@ async function revokeJwtAccessToken(ctx, opts, token) {
2530
2328
  }
2531
2329
  throw new Error(error);
2532
2330
  }
2331
+ throw new APIError$1("BAD_REQUEST", {
2332
+ error_description: "JWT access tokens are self-contained and cannot be revoked server-side",
2333
+ error: "unsupported_token_type"
2334
+ });
2533
2335
  }
2534
2336
  /**
2535
2337
  * Searches for an opaque access token in the database and validates it
@@ -2591,7 +2393,7 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
2591
2393
  }
2592
2394
  if (!refreshToken.clientId || refreshToken.clientId !== clientId) return null;
2593
2395
  const iat = Math.floor(Date.now() / 1e3);
2594
- if (!await ctx.context.adapter.update({
2396
+ if (!await ctx.context.adapter.incrementOne({
2595
2397
  model: "oauthRefreshToken",
2596
2398
  where: [{
2597
2399
  field: "id",
@@ -2601,7 +2403,8 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
2601
2403
  operator: "eq",
2602
2404
  value: null
2603
2405
  }],
2604
- update: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
2406
+ increment: {},
2407
+ set: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
2605
2408
  })) {
2606
2409
  await invalidateRefreshFamily(ctx, clientId, refreshToken.userId);
2607
2410
  throw new APIError$1("BAD_REQUEST", {
@@ -2625,7 +2428,9 @@ async function revokeAccessToken(ctx, opts, clientId, token) {
2625
2428
  try {
2626
2429
  return await revokeJwtAccessToken(ctx, opts, token);
2627
2430
  } catch (err) {
2628
- if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
2431
+ if (err instanceof APIError$1) {
2432
+ if (err.body?.error === "unsupported_token_type") throw err;
2433
+ } else if (err instanceof Error) throw err;
2629
2434
  else throw new Error(err);
2630
2435
  }
2631
2436
  try {
@@ -2642,23 +2447,23 @@ async function revokeAccessToken(ctx, opts, clientId, token) {
2642
2447
  async function revokeEndpoint(ctx, opts) {
2643
2448
  let { token, token_type_hint } = ctx.body;
2644
2449
  if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
2645
- const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/revoke`));
2450
+ const { clientId: client_id, clientSecret: client_secret, preVerified, authMethod } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/revoke`));
2646
2451
  if (!client_id) throw new APIError$1("UNAUTHORIZED", {
2647
2452
  error_description: "missing required credentials",
2648
2453
  error: "invalid_client"
2649
2454
  });
2650
- if (typeof token === "string" && token.startsWith("Bearer ")) token = token.replace("Bearer ", "");
2455
+ if (typeof token === "string") token = stripAccessTokenAuthorizationScheme(token);
2651
2456
  if (!token?.length) throw new APIError$1("BAD_REQUEST", {
2652
2457
  error_description: "missing a required token for introspection",
2653
2458
  error: "invalid_request"
2654
2459
  });
2655
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
2460
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, void 0, authMethod);
2656
2461
  try {
2657
2462
  if (token_type_hint === void 0 || token_type_hint === "access_token") try {
2658
2463
  return await revokeAccessToken(ctx, opts, client.clientId, token);
2659
2464
  } catch (error) {
2660
2465
  if (error instanceof APIError$1) {
2661
- if (token_type_hint === "access_token") throw error;
2466
+ if (token_type_hint === "access_token" || error.body?.error === "unsupported_token_type") throw error;
2662
2467
  } else if (error instanceof Error) throw error;
2663
2468
  else throw new Error(error);
2664
2469
  }
@@ -2676,6 +2481,7 @@ async function revokeEndpoint(ctx, opts) {
2676
2481
  });
2677
2482
  } catch (error) {
2678
2483
  if (error instanceof APIError$1) {
2484
+ if (error.body?.error === "unsupported_token_type") throw error;
2679
2485
  if (error.name === "BAD_REQUEST") return null;
2680
2486
  throw error;
2681
2487
  } else if (error instanceof Error) {
@@ -2784,6 +2590,14 @@ const schema = {
2784
2590
  type: "string[]",
2785
2591
  required: false
2786
2592
  },
2593
+ backchannelLogoutUri: {
2594
+ type: "string",
2595
+ required: false
2596
+ },
2597
+ backchannelLogoutSessionRequired: {
2598
+ type: "boolean",
2599
+ required: false
2600
+ },
2787
2601
  tokenEndpointAuthMethod: {
2788
2602
  type: "string",
2789
2603
  required: false
@@ -2816,6 +2630,11 @@ const schema = {
2816
2630
  type: "boolean",
2817
2631
  required: false
2818
2632
  },
2633
+ dpopBoundAccessTokens: {
2634
+ type: "boolean",
2635
+ required: false,
2636
+ defaultValue: false
2637
+ },
2819
2638
  referenceId: {
2820
2639
  type: "string",
2821
2640
  required: false
@@ -2826,6 +2645,104 @@ const schema = {
2826
2645
  }
2827
2646
  }
2828
2647
  },
2648
+ oauthResource: {
2649
+ modelName: "oauthResource",
2650
+ fields: {
2651
+ identifier: {
2652
+ type: "string",
2653
+ required: true,
2654
+ unique: true
2655
+ },
2656
+ name: {
2657
+ type: "string",
2658
+ required: true
2659
+ },
2660
+ accessTokenTtl: {
2661
+ type: "number",
2662
+ required: false
2663
+ },
2664
+ refreshTokenTtl: {
2665
+ type: "number",
2666
+ required: false
2667
+ },
2668
+ signingAlgorithm: {
2669
+ type: "string",
2670
+ required: false
2671
+ },
2672
+ signingKeyId: {
2673
+ type: "string",
2674
+ required: false
2675
+ },
2676
+ allowedScopes: {
2677
+ type: "string[]",
2678
+ required: false
2679
+ },
2680
+ customClaims: {
2681
+ type: "json",
2682
+ required: false
2683
+ },
2684
+ dpopBoundAccessTokensRequired: {
2685
+ type: "boolean",
2686
+ required: false,
2687
+ defaultValue: false
2688
+ },
2689
+ disabled: {
2690
+ type: "boolean",
2691
+ required: false,
2692
+ defaultValue: false
2693
+ },
2694
+ createdAt: {
2695
+ type: "date",
2696
+ required: false
2697
+ },
2698
+ updatedAt: {
2699
+ type: "date",
2700
+ required: false
2701
+ },
2702
+ policyVersion: {
2703
+ type: "number",
2704
+ required: false,
2705
+ defaultValue: 1
2706
+ },
2707
+ metadata: {
2708
+ type: "json",
2709
+ required: false
2710
+ }
2711
+ }
2712
+ },
2713
+ oauthClientResource: {
2714
+ modelName: "oauthClientResource",
2715
+ fields: {
2716
+ clientId: {
2717
+ type: "string",
2718
+ required: true,
2719
+ references: {
2720
+ model: "oauthClient",
2721
+ field: "clientId",
2722
+ onDelete: "cascade"
2723
+ },
2724
+ index: true
2725
+ },
2726
+ resourceId: {
2727
+ type: "string",
2728
+ required: true,
2729
+ references: {
2730
+ model: "oauthResource",
2731
+ field: "identifier",
2732
+ onDelete: "cascade"
2733
+ },
2734
+ index: true
2735
+ },
2736
+ metadata: {
2737
+ type: "json",
2738
+ required: false
2739
+ },
2740
+ createdAt: {
2741
+ type: "date",
2742
+ required: false
2743
+ }
2744
+ }
2745
+ },
2829
2746
  oauthRefreshToken: { fields: {
2830
2747
  token: {
2831
2748
  type: "string",
@@ -2878,6 +2795,10 @@ const schema = {
2878
2795
  type: "date",
2879
2796
  required: false
2880
2797
  },
2798
+ confirmation: {
2799
+ type: "json",
2800
+ required: false
2801
+ },
2881
2802
  scopes: {
2882
2803
  type: "string[]",
2883
2804
  required: true
@@ -2937,6 +2858,14 @@ const schema = {
2937
2858
  },
2938
2859
  expiresAt: { type: "date" },
2939
2860
  createdAt: { type: "date" },
2861
+ revoked: {
2862
+ type: "date",
2863
+ required: false
2864
+ },
2865
+ confirmation: {
2866
+ type: "json",
2867
+ required: false
2868
+ },
2940
2869
  scopes: {
2941
2870
  type: "string[]",
2942
2871
  required: true
@@ -2979,12 +2908,36 @@ const schema = {
2979
2908
  createdAt: { type: "date" },
2980
2909
  updatedAt: { type: "date" }
2981
2910
  }
2911
+ },
2912
+ oauthClientAssertion: {
2913
+ modelName: "oauthClientAssertion",
2914
+ fields: { expiresAt: {
2915
+ type: "date",
2916
+ required: true
2917
+ } }
2982
2918
  }
2983
2919
  };
2984
2920
  //#endregion
2985
2921
  //#region src/oauth.ts
2922
+ /**
2923
+ * Default scopes advertised and accepted when a configuration sets none. Shared
2924
+ * with the MCP preset so the resource metadata it serves matches what the
2925
+ * authorization-server metadata advertises.
2926
+ */
2927
+ const DEFAULT_OAUTH_SCOPES = [
2928
+ "openid",
2929
+ "profile",
2930
+ "email",
2931
+ "offline_access"
2932
+ ];
2986
2933
  const oAuthState = defineRequestState(() => null);
2987
2934
  const getOAuthProviderState = oAuthState.get;
2935
+ const signedQueryIssuedAtMsKey = "signedQueryIssuedAtMs";
2936
+ function getServerContextSignedQueryIssuedAt(value) {
2937
+ const issuedAtMs = typeof value === "number" ? value : typeof value === "string" ? Number(value) : void 0;
2938
+ if (!issuedAtMs || !Number.isFinite(issuedAtMs) || issuedAtMs <= 0) return;
2939
+ return new Date(issuedAtMs);
2940
+ }
2988
2941
  /**
2989
2942
  * oAuth 2.1 provider plugin for Better Auth.
2990
2943
  *
@@ -2998,12 +2951,7 @@ const oauthProvider = (options) => {
2998
2951
  const _allowedScopes = clientRegistrationAllowedScopes ? new Set([...clientRegistrationAllowedScopes, ...options.clientRegistrationDefaultScopes]) : new Set([...options.clientRegistrationDefaultScopes]);
2999
2952
  clientRegistrationAllowedScopes = Array.from(_allowedScopes);
3000
2953
  }
3001
- const scopes = new Set((options.scopes ?? [
3002
- "openid",
3003
- "profile",
3004
- "email",
3005
- "offline_access"
3006
- ]).filter((val) => val.length));
2954
+ const scopes = new Set((options.scopes ?? DEFAULT_OAUTH_SCOPES).filter((val) => val.length));
3007
2955
  if (clientRegistrationAllowedScopes) {
3008
2956
  for (const sc of clientRegistrationAllowedScopes) if (!scopes.has(sc)) throw new BetterAuthError(`clientRegistrationAllowedScope ${sc} not found in scopes`);
3009
2957
  }
@@ -3045,6 +2993,7 @@ const oauthProvider = (options) => {
3045
2993
  claims: Array.from(claims),
3046
2994
  clientRegistrationAllowedScopes
3047
2995
  };
2996
+ validateOAuthProviderExtensions(opts.extensions);
3048
2997
  if (opts.pairwiseSecret && opts.pairwiseSecret.length < 32) throw new BetterAuthError("pairwiseSecret must be at least 32 characters long for adequate HMAC-SHA256 security");
3049
2998
  if (opts.grantTypes && opts.grantTypes.includes("refresh_token") && !opts.grantTypes.includes("authorization_code")) throw new BetterAuthError("refresh_token grant requires authorization_code grant");
3050
2999
  if (opts.disableJwtPlugin && (opts.storeClientSecret === "hashed" || typeof opts.storeClientSecret === "object" && "hash" in opts.storeClientSecret)) throw new BetterAuthError("unable to store hashed secrets because id tokens will be signed with secret");
@@ -3080,26 +3029,159 @@ const oauthProvider = (options) => {
3080
3029
  }
3081
3030
  if (isAuthServerMetadataRequest) {
3082
3031
  if (opts.scopes?.includes("openid")) return { response: createMetadataResponse(oidcServerMetadata(endpointCtx, opts)) };
3083
- return { response: createMetadataResponse({
3084
- ...authServerMetadata(endpointCtx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx)?.options, {
3085
- scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
3086
- dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
3087
- public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
3088
- grant_types_supported: opts.grantTypes,
3089
- jwt_disabled: opts.disableJwtPlugin
3090
- }),
3091
- ...mergeDiscoveryMetadata(opts.clientDiscovery)
3092
- }) };
3032
+ return { response: createMetadataResponse(oauthAuthorizationServerMetadata(endpointCtx, opts)) };
3093
3033
  }
3094
3034
  if (isOpenIdConfigRequest) return { response: createMetadataResponse(oidcServerMetadata(endpointCtx, opts)) };
3095
3035
  };
3036
+ const oauth2AuthorizeEndpoint = createOAuthEndpoint("/oauth2/authorize", {
3037
+ method: "GET",
3038
+ query: authorizationQuerySchema,
3039
+ redirectOnError: authorizeRedirectOnError(opts),
3040
+ errorCodesByField: {
3041
+ response_type: { invalid: "unsupported_response_type" },
3042
+ resource: { invalid: "invalid_target" }
3043
+ },
3044
+ metadata: { openapi: {
3045
+ description: "Authorize an OAuth2 request",
3046
+ parameters: [
3047
+ {
3048
+ name: "response_type",
3049
+ in: "query",
3050
+ required: false,
3051
+ schema: { type: "string" },
3052
+ description: "OAuth2 response type (e.g., 'code')"
3053
+ },
3054
+ {
3055
+ name: "client_id",
3056
+ in: "query",
3057
+ required: true,
3058
+ schema: { type: "string" },
3059
+ description: "OAuth2 client ID"
3060
+ },
3061
+ {
3062
+ name: "redirect_uri",
3063
+ in: "query",
3064
+ required: false,
3065
+ schema: {
3066
+ type: "string",
3067
+ format: "uri"
3068
+ },
3069
+ description: "OAuth2 redirect URI"
3070
+ },
3071
+ {
3072
+ name: "scope",
3073
+ in: "query",
3074
+ required: false,
3075
+ schema: { type: "string" },
3076
+ description: "OAuth2 scopes (space-separated)"
3077
+ },
3078
+ {
3079
+ name: "state",
3080
+ in: "query",
3081
+ required: false,
3082
+ schema: { type: "string" },
3083
+ description: "OAuth2 state parameter"
3084
+ },
3085
+ {
3086
+ name: "request_uri",
3087
+ in: "query",
3088
+ required: false,
3089
+ schema: { type: "string" },
3090
+ description: "Pushed Authorization Request URI referencing stored parameters"
3091
+ },
3092
+ {
3093
+ name: "code_challenge",
3094
+ in: "query",
3095
+ required: false,
3096
+ schema: { type: "string" },
3097
+ description: "PKCE code challenge"
3098
+ },
3099
+ {
3100
+ name: "code_challenge_method",
3101
+ in: "query",
3102
+ required: false,
3103
+ schema: { type: "string" },
3104
+ description: "PKCE code challenge method"
3105
+ },
3106
+ {
3107
+ name: "nonce",
3108
+ in: "query",
3109
+ required: false,
3110
+ schema: { type: "string" },
3111
+ description: "OpenID Connect nonce"
3112
+ },
3113
+ {
3114
+ name: "max_age",
3115
+ in: "query",
3116
+ required: false,
3117
+ schema: {
3118
+ type: "integer",
3119
+ minimum: 0
3120
+ },
3121
+ description: "Maximum authentication age in seconds; forces re-authentication when exceeded"
3122
+ },
3123
+ {
3124
+ name: "resource",
3125
+ in: "query",
3126
+ required: false,
3127
+ schema: {
3128
+ type: "array",
3129
+ items: { type: "string" }
3130
+ },
3131
+ description: "Requested protected resource(s) for the access token. May be supplied multiple times as repeated 'resource' query parameters (RFC 8707) or as an array of strings."
3132
+ },
3133
+ {
3134
+ name: "prompt",
3135
+ in: "query",
3136
+ required: false,
3137
+ schema: { type: "string" },
3138
+ description: "OAuth2 prompt parameter"
3139
+ }
3140
+ ],
3141
+ responses: {
3142
+ "302": {
3143
+ description: "Redirect to client with code or error",
3144
+ headers: { Location: {
3145
+ description: "Redirect URI with code or error",
3146
+ schema: {
3147
+ type: "string",
3148
+ format: "uri"
3149
+ }
3150
+ } }
3151
+ },
3152
+ "400": {
3153
+ description: "Invalid request",
3154
+ content: { "application/json": { schema: {
3155
+ type: "object",
3156
+ properties: {
3157
+ error: { type: "string" },
3158
+ error_description: { type: "string" },
3159
+ state: { type: "string" }
3160
+ },
3161
+ required: ["error"]
3162
+ } } }
3163
+ }
3164
+ }
3165
+ } }
3166
+ }, async (ctx) => {
3167
+ return authorizeEndpoint(ctx, opts, ctx.authorizeSettings ?? { isAuthorize: true });
3168
+ });
3169
+ const runOAuth2Authorize = (ctx, settings) => dispatchAuthEndpoint(oauth2AuthorizeEndpoint, {
3170
+ ...ctx,
3171
+ asResponse: false,
3172
+ returnHeaders: false,
3173
+ returnStatus: false,
3174
+ authorizeSettings: settings ?? {}
3175
+ });
3096
3176
  return {
3097
3177
  id: "oauth-provider",
3098
3178
  version: PACKAGE_VERSION,
3099
3179
  options: opts,
3100
3180
  onRequest: handleIssuerMetadataRequest,
3101
- init: (ctx) => {
3181
+ init: async (ctx) => {
3102
3182
  if (ctx.options.secondaryStorage && ctx.options.session?.storeSessionInDatabase !== true) throw new BetterAuthError("OAuth Provider requires `session.storeSessionInDatabase: true` when using secondaryStorage");
3183
+ await seedResources(ctx, opts);
3184
+ logEnforcePerClientResourcesResolution(opts);
3103
3185
  if (!opts.disableJwtPlugin) {
3104
3186
  const jwtPluginOptions = getJwtPlugin(ctx)?.options;
3105
3187
  const issuer = jwtPluginOptions?.jwt?.issuer ?? ctx.baseURL;
@@ -3108,12 +3190,20 @@ const oauthProvider = (options) => {
3108
3190
  try {
3109
3191
  issuerPath = new URL(issuer).pathname;
3110
3192
  } catch (error) {
3111
- if (isDynamicBaseURLInit && issuer === "") return;
3112
- throw error;
3193
+ if (!isDynamicBaseURLInit || issuer !== "") throw error;
3113
3194
  }
3114
- if (!opts.silenceWarnings?.oauthAuthServerConfig && !(ctx.options.basePath === "/" && issuerPath === "/")) logger.warn(`Please ensure '/.well-known/oauth-authorization-server${issuerPath === "/" ? "" : issuerPath}' exists. Upon completion, clear with silenceWarnings.oauthAuthServerConfig.`);
3115
- if (!opts.silenceWarnings?.openidConfig && ctx.options.basePath !== issuerPath && opts.scopes?.includes("openid")) logger.warn(`Please ensure '${issuerPath}${issuerPath.endsWith("/") ? "" : "/"}.well-known/openid-configuration' exists. Upon completion, clear with silenceWarnings.openidConfig.`);
3195
+ if (issuerPath !== void 0 && !opts.silenceWarnings?.oauthAuthServerConfig && !(ctx.options.basePath === "/" && issuerPath === "/")) logger.warn(`Please ensure '/.well-known/oauth-authorization-server${issuerPath === "/" ? "" : issuerPath}' exists. Upon completion, clear with silenceWarnings.oauthAuthServerConfig.`);
3196
+ if (issuerPath !== void 0 && !opts.silenceWarnings?.openidConfig && ctx.options.basePath !== issuerPath && opts.scopes?.includes("openid")) logger.warn(`Please ensure '${issuerPath}${issuerPath.endsWith("/") ? "" : "/"}.well-known/openid-configuration' exists. Upon completion, clear with silenceWarnings.openidConfig.`);
3116
3197
  }
3198
+ return { options: { databaseHooks: { session: { delete: { async before(session, hookCtx) {
3199
+ if (!hookCtx) return;
3200
+ const plan = await revokeAndPlanBackchannelLogout(hookCtx, opts, {
3201
+ sessionId: session.id,
3202
+ userId: session.userId
3203
+ });
3204
+ if (!plan) return;
3205
+ await hookCtx.context.runInBackgroundOrAwait(deliverBackchannelLogoutTokens(hookCtx, plan));
3206
+ } } } } } };
3117
3207
  },
3118
3208
  hooks: {
3119
3209
  before: [{
@@ -3135,11 +3225,10 @@ const oauthProvider = (options) => {
3135
3225
  signedQueryIssuedAt: signedQueryIssuedAt ?? void 0,
3136
3226
  postLoginClearedForSession
3137
3227
  });
3138
- if (ctx.path === "/sign-in/social") {
3139
- if (ctx.body.additionalData?.query) return;
3140
- if (!ctx.body.additionalData) ctx.body.additionalData = {};
3141
- ctx.body.additionalData.query = queryParams.toString();
3142
- }
3228
+ if (ctx.path === "/sign-in/social") await addOAuthServerContext({
3229
+ query: queryParams.toString(),
3230
+ ...signedQueryIssuedAt ? { [signedQueryIssuedAtMsKey]: signedQueryIssuedAt.getTime() } : {}
3231
+ });
3143
3232
  })
3144
3233
  }],
3145
3234
  after: [{
@@ -3149,7 +3238,9 @@ const oauthProvider = (options) => {
3149
3238
  handler: createAuthMiddleware(async (ctx) => {
3150
3239
  const sessionToken = parseSetCookieHeader(ctx.context.responseHeaders?.get("set-cookie") || "").get(ctx.context.authCookies.sessionToken.name)?.value.split(".")[0];
3151
3240
  if (!sessionToken) return;
3152
- const _query = (await oAuthState.get())?.query ?? (await getOAuthState())?.query;
3241
+ const oauthRequest = await oAuthState.get();
3242
+ const serverContext = (await getOAuthState())?.serverContext;
3243
+ const _query = oauthRequest?.query ?? serverContext?.query;
3153
3244
  if (!_query) return;
3154
3245
  const query = new URLSearchParams(_query);
3155
3246
  const session = await ctx.context.internalAdapter.findSession(sessionToken);
@@ -3158,8 +3249,11 @@ const oauthProvider = (options) => {
3158
3249
  const secFetchMode = ctx.request?.headers?.get("sec-fetch-mode")?.toLowerCase();
3159
3250
  const acceptHeader = ctx.request?.headers?.get("accept")?.toLowerCase() ?? "";
3160
3251
  if (!(secFetchMode === "navigate" || !secFetchMode && (acceptHeader.includes("text/html") || acceptHeader.includes("application/xhtml+xml")))) ctx.headers?.set("accept", "application/json");
3161
- ctx.query = searchParamsToQuery(removePromptFromQuery(query, "login"));
3162
- return await authorizeEndpoint(ctx, opts);
3252
+ const signedQueryIssuedAt = oauthRequest?.signedQueryIssuedAt ?? getServerContextSignedQueryIssuedAt(serverContext?.[signedQueryIssuedAtMsKey]);
3253
+ let authorizationQuery = removePromptFromQuery(query, "login");
3254
+ if (isSessionFreshForSignedQuery(session.session.createdAt, signedQueryIssuedAt)) authorizationQuery = removeMaxAgeFromQuery(authorizationQuery);
3255
+ ctx.query = searchParamsToQuery(authorizationQuery);
3256
+ return await runOAuth2Authorize(ctx);
3163
3257
  })
3164
3258
  }]
3165
3259
  },
@@ -3169,16 +3263,7 @@ const oauthProvider = (options) => {
3169
3263
  metadata: { SERVER_ONLY: true }
3170
3264
  }, async (ctx) => {
3171
3265
  if (opts.scopes && opts.scopes.includes("openid")) return oidcServerMetadata(ctx, opts);
3172
- else return {
3173
- ...authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
3174
- scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
3175
- dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
3176
- public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
3177
- grant_types_supported: opts.grantTypes,
3178
- jwt_disabled: opts.disableJwtPlugin
3179
- }),
3180
- ...mergeDiscoveryMetadata(opts.clientDiscovery)
3181
- };
3266
+ else return oauthAuthorizationServerMetadata(ctx, opts);
3182
3267
  }),
3183
3268
  getOpenIdConfig: createAuthEndpoint("/.well-known/openid-configuration", {
3184
3269
  method: "GET",
@@ -3187,149 +3272,7 @@ const oauthProvider = (options) => {
3187
3272
  if (opts.scopes && !opts.scopes.includes("openid")) throw new APIError("NOT_FOUND");
3188
3273
  return oidcServerMetadata(ctx, opts);
3189
3274
  }),
3190
- oauth2Authorize: createOAuthEndpoint("/oauth2/authorize", {
3191
- method: "GET",
3192
- query: z.object({
3193
- response_type: z.string().pipe(z.enum(["code"])).optional(),
3194
- client_id: z.string(),
3195
- redirect_uri: SafeUrlSchema.optional(),
3196
- scope: z.string().optional(),
3197
- state: z.string().optional(),
3198
- request_uri: z.string().optional(),
3199
- code_challenge: z.string().optional(),
3200
- code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
3201
- nonce: z.string().optional(),
3202
- resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional(),
3203
- prompt: z.string().pipe(z.enum([
3204
- "none",
3205
- "consent",
3206
- "login",
3207
- "create",
3208
- "select_account",
3209
- "login consent",
3210
- "select_account consent"
3211
- ])).optional()
3212
- }),
3213
- redirectOnError: authorizeRedirectOnError(opts),
3214
- errorCodesByField: {
3215
- response_type: { invalid: "unsupported_response_type" },
3216
- resource: { invalid: "invalid_target" }
3217
- },
3218
- metadata: { openapi: {
3219
- description: "Authorize an OAuth2 request",
3220
- parameters: [
3221
- {
3222
- name: "response_type",
3223
- in: "query",
3224
- required: false,
3225
- schema: { type: "string" },
3226
- description: "OAuth2 response type (e.g., 'code')"
3227
- },
3228
- {
3229
- name: "client_id",
3230
- in: "query",
3231
- required: true,
3232
- schema: { type: "string" },
3233
- description: "OAuth2 client ID"
3234
- },
3235
- {
3236
- name: "redirect_uri",
3237
- in: "query",
3238
- required: false,
3239
- schema: {
3240
- type: "string",
3241
- format: "uri"
3242
- },
3243
- description: "OAuth2 redirect URI"
3244
- },
3245
- {
3246
- name: "scope",
3247
- in: "query",
3248
- required: false,
3249
- schema: { type: "string" },
3250
- description: "OAuth2 scopes (space-separated)"
3251
- },
3252
- {
3253
- name: "state",
3254
- in: "query",
3255
- required: false,
3256
- schema: { type: "string" },
3257
- description: "OAuth2 state parameter"
3258
- },
3259
- {
3260
- name: "request_uri",
3261
- in: "query",
3262
- required: false,
3263
- schema: { type: "string" },
3264
- description: "Pushed Authorization Request URI referencing stored parameters"
3265
- },
3266
- {
3267
- name: "code_challenge",
3268
- in: "query",
3269
- required: false,
3270
- schema: { type: "string" },
3271
- description: "PKCE code challenge"
3272
- },
3273
- {
3274
- name: "code_challenge_method",
3275
- in: "query",
3276
- required: false,
3277
- schema: { type: "string" },
3278
- description: "PKCE code challenge method"
3279
- },
3280
- {
3281
- name: "nonce",
3282
- in: "query",
3283
- required: false,
3284
- schema: { type: "string" },
3285
- description: "OpenID Connect nonce"
3286
- },
3287
- {
3288
- name: "resource",
3289
- in: "query",
3290
- required: false,
3291
- schema: {
3292
- type: "array",
3293
- items: { type: "string" }
3294
- },
3295
- description: "Requested token resource(s) (ie audience) to obtain a JWT formatted access token. May be supplied multiple times as repeated 'resource' query parameters (RFC 8707) or as an array of strings."
3296
- },
3297
- {
3298
- name: "prompt",
3299
- in: "query",
3300
- required: false,
3301
- schema: { type: "string" },
3302
- description: "OAuth2 prompt parameter"
3303
- }
3304
- ],
3305
- responses: {
3306
- "302": {
3307
- description: "Redirect to client with code or error",
3308
- headers: { Location: {
3309
- description: "Redirect URI with code or error",
3310
- schema: {
3311
- type: "string",
3312
- format: "uri"
3313
- }
3314
- } }
3315
- },
3316
- "400": {
3317
- description: "Invalid request",
3318
- content: { "application/json": { schema: {
3319
- type: "object",
3320
- properties: {
3321
- error: { type: "string" },
3322
- error_description: { type: "string" },
3323
- state: { type: "string" }
3324
- },
3325
- required: ["error"]
3326
- } } }
3327
- }
3328
- }
3329
- } }
3330
- }, async (ctx) => {
3331
- return authorizeEndpoint(ctx, opts, { isAuthorize: true });
3332
- }),
3275
+ oauth2Authorize: oauth2AuthorizeEndpoint,
3333
3276
  oauth2Consent: createAuthEndpoint("/oauth2/consent", {
3334
3277
  method: "POST",
3335
3278
  body: z.object({
@@ -3354,7 +3297,7 @@ const oauthProvider = (options) => {
3354
3297
  } }
3355
3298
  } }
3356
3299
  }, async (ctx) => {
3357
- return consentEndpoint(ctx, opts);
3300
+ return consentEndpoint(ctx, opts, runOAuth2Authorize);
3358
3301
  }),
3359
3302
  oauth2Continue: createAuthEndpoint("/oauth2/continue", {
3360
3303
  method: "POST",
@@ -3381,16 +3324,13 @@ const oauthProvider = (options) => {
3381
3324
  } }
3382
3325
  } }
3383
3326
  }, async (ctx) => {
3384
- return continueEndpoint(ctx, opts);
3327
+ return continueEndpoint(ctx, runOAuth2Authorize);
3385
3328
  }),
3386
3329
  oauth2Token: createOAuthEndpoint("/oauth2/token", {
3387
3330
  method: "POST",
3331
+ cloneRequest: true,
3388
3332
  body: z.object({
3389
- grant_type: z.string().pipe(z.enum([
3390
- "authorization_code",
3391
- "client_credentials",
3392
- "refresh_token"
3393
- ])),
3333
+ grant_type: z.string().trim().min(1),
3394
3334
  client_id: z.string().optional(),
3395
3335
  client_secret: z.string().optional(),
3396
3336
  client_assertion: z.string().optional(),
@@ -3401,7 +3341,7 @@ const oauthProvider = (options) => {
3401
3341
  refresh_token: z.string().optional(),
3402
3342
  resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional(),
3403
3343
  scope: z.string().optional()
3404
- }),
3344
+ }).passthrough(),
3405
3345
  errorCodesByField: {
3406
3346
  grant_type: {
3407
3347
  missing: "invalid_request",
@@ -3410,9 +3350,17 @@ const oauthProvider = (options) => {
3410
3350
  resource: { invalid: "invalid_target" }
3411
3351
  },
3412
3352
  metadata: {
3353
+ noStore: true,
3413
3354
  allowedMediaTypes: ["application/x-www-form-urlencoded"],
3414
3355
  openapi: {
3415
3356
  description: "Obtain an OAuth2.1 access token",
3357
+ parameters: [{
3358
+ name: "DPoP",
3359
+ in: "header",
3360
+ required: false,
3361
+ schema: { type: "string" },
3362
+ description: "RFC 9449 DPoP proof JWT for issuing DPoP-bound tokens"
3363
+ }],
3416
3364
  requestBody: {
3417
3365
  required: true,
3418
3366
  content: { "application/json": { schema: {
@@ -3420,11 +3368,6 @@ const oauthProvider = (options) => {
3420
3368
  properties: {
3421
3369
  grant_type: {
3422
3370
  type: "string",
3423
- enum: [
3424
- "authorization_code",
3425
- "client_credentials",
3426
- "refresh_token"
3427
- ],
3428
3371
  description: "OAuth2 grant type"
3429
3372
  },
3430
3373
  client_id: {
@@ -3461,7 +3404,7 @@ const oauthProvider = (options) => {
3461
3404
  items: { type: "string" },
3462
3405
  description: "Multiple resources (URLs)"
3463
3406
  }],
3464
- description: "Requested token resource(s) (ie audience) to obtain a JWT formatted access token"
3407
+ description: "Requested protected resource(s) for the access token"
3465
3408
  },
3466
3409
  scope: {
3467
3410
  type: "string",
@@ -3484,7 +3427,7 @@ const oauthProvider = (options) => {
3484
3427
  token_type: {
3485
3428
  type: "string",
3486
3429
  description: "The type of the token issued",
3487
- enum: ["Bearer"]
3430
+ enum: ["Bearer", "DPoP"]
3488
3431
  },
3489
3432
  expires_in: {
3490
3433
  type: "number",
@@ -3526,6 +3469,10 @@ const oauthProvider = (options) => {
3526
3469
  }
3527
3470
  }
3528
3471
  }, async (ctx) => {
3472
+ if (ctx.request) {
3473
+ const repeated = await extractRepeatedResourceFromForm(ctx.request);
3474
+ if (repeated && repeated.length > 1) ctx.body.resource = repeated;
3475
+ }
3529
3476
  return tokenEndpoint(ctx, opts);
3530
3477
  }),
3531
3478
  oauth2Introspect: createOAuthEndpoint("/oauth2/introspect", {
@@ -3539,6 +3486,7 @@ const oauthProvider = (options) => {
3539
3486
  token_type_hint: z.string().optional()
3540
3487
  }),
3541
3488
  metadata: {
3489
+ noStore: true,
3542
3490
  allowedMediaTypes: ["application/x-www-form-urlencoded"],
3543
3491
  openapi: {
3544
3492
  description: "Introspect an OAuth2 access or refresh token",
@@ -3709,91 +3657,100 @@ const oauthProvider = (options) => {
3709
3657
  return revokeEndpoint(ctx, opts);
3710
3658
  }),
3711
3659
  oauth2UserInfo: createAuthEndpoint("/oauth2/userinfo", {
3712
- method: "GET",
3713
- metadata: { openapi: {
3714
- description: "Get OpenID Connect user information (UserInfo endpoint)",
3715
- security: [{ bearerAuth: [] }, { OAuth2: [
3716
- "openid",
3717
- "profile",
3718
- "email"
3719
- ] }],
3720
- parameters: [{
3721
- name: "Authorization",
3722
- in: "header",
3723
- required: false,
3724
- schema: { type: "string" },
3725
- description: "Bearer access token"
3726
- }],
3727
- responses: {
3728
- "200": {
3729
- description: "User information retrieved successfully",
3730
- content: { "application/json": { schema: {
3731
- type: "object",
3732
- properties: {
3733
- sub: {
3734
- type: "string",
3735
- description: "Subject identifier (user ID)"
3736
- },
3737
- email: {
3738
- type: "string",
3739
- format: "email",
3740
- nullable: true,
3741
- description: "User's email address, included if 'email' scope is granted"
3742
- },
3743
- name: {
3744
- type: "string",
3745
- nullable: true,
3746
- description: "User's full name, included if 'profile' scope is granted"
3747
- },
3748
- picture: {
3749
- type: "string",
3750
- format: "uri",
3751
- nullable: true,
3752
- description: "User's profile picture URL, included if 'profile' scope is granted"
3660
+ method: ["GET", "POST"],
3661
+ metadata: {
3662
+ noStore: true,
3663
+ openapi: {
3664
+ description: "Get OpenID Connect user information (UserInfo endpoint)",
3665
+ security: [{ bearerAuth: [] }, { OAuth2: [
3666
+ "openid",
3667
+ "profile",
3668
+ "email"
3669
+ ] }],
3670
+ parameters: [{
3671
+ name: "Authorization",
3672
+ in: "header",
3673
+ required: false,
3674
+ schema: { type: "string" },
3675
+ description: "Bearer or DPoP access token"
3676
+ }, {
3677
+ name: "DPoP",
3678
+ in: "header",
3679
+ required: false,
3680
+ schema: { type: "string" },
3681
+ description: "RFC 9449 DPoP proof JWT when using a DPoP-bound access token"
3682
+ }],
3683
+ responses: {
3684
+ "200": {
3685
+ description: "User information retrieved successfully",
3686
+ content: { "application/json": { schema: {
3687
+ type: "object",
3688
+ properties: {
3689
+ sub: {
3690
+ type: "string",
3691
+ description: "Subject identifier (user ID)"
3692
+ },
3693
+ email: {
3694
+ type: "string",
3695
+ format: "email",
3696
+ nullable: true,
3697
+ description: "User's email address, included if 'email' scope is granted"
3698
+ },
3699
+ name: {
3700
+ type: "string",
3701
+ nullable: true,
3702
+ description: "User's full name, included if 'profile' scope is granted"
3703
+ },
3704
+ picture: {
3705
+ type: "string",
3706
+ format: "uri",
3707
+ nullable: true,
3708
+ description: "User's profile picture URL, included if 'profile' scope is granted"
3709
+ },
3710
+ given_name: {
3711
+ type: "string",
3712
+ nullable: true,
3713
+ description: "User's given name, included if 'profile' scope is granted"
3714
+ },
3715
+ family_name: {
3716
+ type: "string",
3717
+ nullable: true,
3718
+ description: "User's family name, included if 'profile' scope is granted"
3719
+ },
3720
+ email_verified: {
3721
+ type: "boolean",
3722
+ nullable: true,
3723
+ description: "Whether the email is verified, included if 'email' scope is granted"
3724
+ }
3753
3725
  },
3754
- given_name: {
3755
- type: "string",
3756
- nullable: true,
3757
- description: "User's given name, included if 'profile' scope is granted"
3726
+ required: ["sub"]
3727
+ } } }
3728
+ },
3729
+ "401": {
3730
+ description: "Unauthorized - invalid or missing access token",
3731
+ content: { "application/json": { schema: {
3732
+ type: "object",
3733
+ properties: {
3734
+ error: { type: "string" },
3735
+ error_description: { type: "string" }
3758
3736
  },
3759
- family_name: {
3760
- type: "string",
3761
- nullable: true,
3762
- description: "User's family name, included if 'profile' scope is granted"
3737
+ required: ["error"]
3738
+ } } }
3739
+ },
3740
+ "403": {
3741
+ description: "Forbidden - insufficient scope",
3742
+ content: { "application/json": { schema: {
3743
+ type: "object",
3744
+ properties: {
3745
+ error: { type: "string" },
3746
+ error_description: { type: "string" }
3763
3747
  },
3764
- email_verified: {
3765
- type: "boolean",
3766
- nullable: true,
3767
- description: "Whether the email is verified, included if 'email' scope is granted"
3768
- }
3769
- },
3770
- required: ["sub"]
3771
- } } }
3772
- },
3773
- "401": {
3774
- description: "Unauthorized - invalid or missing access token",
3775
- content: { "application/json": { schema: {
3776
- type: "object",
3777
- properties: {
3778
- error: { type: "string" },
3779
- error_description: { type: "string" }
3780
- },
3781
- required: ["error"]
3782
- } } }
3783
- },
3784
- "403": {
3785
- description: "Forbidden - insufficient scope",
3786
- content: { "application/json": { schema: {
3787
- type: "object",
3788
- properties: {
3789
- error: { type: "string" },
3790
- error_description: { type: "string" }
3791
- },
3792
- required: ["error"]
3793
- } } }
3748
+ required: ["error"]
3749
+ } } }
3750
+ }
3794
3751
  }
3795
3752
  }
3796
- } }
3753
+ }
3797
3754
  }, async (ctx) => {
3798
3755
  return userInfoEndpoint(ctx, opts);
3799
3756
  }),
@@ -3830,183 +3787,149 @@ const oauthProvider = (options) => {
3830
3787
  }),
3831
3788
  registerOAuthClient: createOAuthEndpoint("/oauth2/register", {
3832
3789
  method: "POST",
3833
- body: z.object({
3834
- redirect_uris: z.array(SafeUrlSchema).min(1).min(1),
3835
- scope: z.string().optional(),
3836
- client_name: z.string().optional(),
3837
- client_uri: z.string().optional(),
3838
- logo_uri: z.string().optional(),
3839
- contacts: z.array(z.string().min(1)).min(1).optional(),
3840
- tos_uri: z.string().optional(),
3841
- policy_uri: z.string().optional(),
3842
- software_id: z.string().optional(),
3843
- software_version: z.string().optional(),
3844
- software_statement: z.string().optional(),
3845
- post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
3846
- token_endpoint_auth_method: z.enum([
3847
- "none",
3848
- "client_secret_basic",
3849
- "client_secret_post",
3850
- "private_key_jwt"
3851
- ]).default("client_secret_basic").optional(),
3852
- jwks: z.union([z.array(z.record(z.string(), z.unknown())).min(1), z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) })]).optional(),
3853
- jwks_uri: z.string().optional(),
3854
- grant_types: z.array(z.enum([
3855
- "authorization_code",
3856
- "client_credentials",
3857
- "refresh_token"
3858
- ])).default(["authorization_code"]).optional(),
3859
- response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
3860
- type: z.enum([
3861
- "web",
3862
- "native",
3863
- "user-agent-based"
3864
- ]).optional(),
3865
- subject_type: z.enum(["public", "pairwise"]).optional(),
3866
- skip_consent: z.never({ error: "skip_consent cannot be set during dynamic client registration" }).optional()
3867
- }),
3790
+ body: clientRegistrationRequestSchema,
3868
3791
  errorCodesByField: {
3869
3792
  redirect_uris: "invalid_redirect_uri",
3870
3793
  post_logout_redirect_uris: "invalid_redirect_uri",
3871
- software_statement: "invalid_software_statement"
3794
+ software_statement: "invalid_software_statement",
3795
+ resources: "invalid_target"
3872
3796
  },
3873
3797
  defaultError: "invalid_client_metadata",
3874
- metadata: { openapi: {
3875
- description: "Register an OAuth2 application",
3876
- responses: { "200": {
3877
- description: "OAuth2 application registered successfully",
3878
- content: { "application/json": { schema: {
3879
- type: "object",
3880
- properties: {
3881
- client_id: {
3882
- type: "string",
3883
- description: "Unique identifier for the client"
3884
- },
3885
- client_secret: {
3886
- type: "string",
3887
- description: "Secret key for the client"
3888
- },
3889
- client_secret_expires_at: {
3890
- type: "number",
3891
- description: "Time the client secret will expire. If 0, the client secret will never expire."
3892
- },
3893
- scope: {
3894
- type: "string",
3895
- description: "Space-separated scopes allowed by the client"
3896
- },
3897
- user_id: {
3898
- type: "string",
3899
- description: "ID of the user who registered the client, null if registered anonymously"
3900
- },
3901
- client_id_issued_at: {
3902
- type: "number",
3903
- description: "Creation timestamp of this client"
3904
- },
3905
- client_name: {
3906
- type: "string",
3907
- description: "Name of the OAuth2 application"
3908
- },
3909
- client_uri: {
3910
- type: "string",
3911
- description: "Name of the OAuth2 application"
3912
- },
3913
- logo_uri: {
3914
- type: "string",
3915
- description: "Icon URL for the application"
3916
- },
3917
- contacts: {
3918
- type: "array",
3919
- items: { type: "string" },
3920
- description: "List representing ways to contact people responsible for this client, typically email addresses"
3921
- },
3922
- tos_uri: {
3923
- type: "string",
3924
- description: "Client's terms of service uri"
3925
- },
3926
- policy_uri: {
3927
- type: "string",
3928
- description: "Client's policy uri"
3929
- },
3930
- software_id: {
3931
- type: "string",
3932
- description: "Unique identifier assigned by the developer to help in the dynamic registration process"
3933
- },
3934
- software_version: {
3935
- type: "string",
3936
- description: "Version identifier for the software_id"
3937
- },
3938
- software_statement: {
3939
- type: "string",
3940
- description: "JWT containing metadata values about the client software as claims"
3941
- },
3942
- redirect_uris: {
3943
- type: "array",
3944
- items: {
3798
+ metadata: {
3799
+ noStore: true,
3800
+ openapi: {
3801
+ description: "Register an OAuth2 application",
3802
+ responses: { "201": {
3803
+ description: "OAuth2 application registered successfully",
3804
+ content: { "application/json": { schema: {
3805
+ type: "object",
3806
+ properties: {
3807
+ client_id: {
3945
3808
  type: "string",
3946
- format: "uri"
3809
+ description: "Unique identifier for the client"
3947
3810
  },
3948
- description: "List of allowed redirect uris"
3949
- },
3950
- post_logout_redirect_uris: {
3951
- type: "array",
3952
- items: {
3811
+ client_secret: {
3953
3812
  type: "string",
3954
- format: "uri"
3813
+ description: "Secret key for the client"
3955
3814
  },
3956
- description: "List of allowed logout redirect uris"
3957
- },
3958
- token_endpoint_auth_method: {
3959
- type: "string",
3960
- description: "Requested authentication method for the token endpoint",
3961
- enum: [
3962
- "none",
3963
- "client_secret_basic",
3964
- "client_secret_post",
3965
- "private_key_jwt"
3966
- ]
3967
- },
3968
- grant_types: {
3969
- type: "array",
3970
- items: {
3815
+ client_secret_expires_at: {
3816
+ type: "number",
3817
+ description: "Time the client secret will expire. If 0, the client secret will never expire."
3818
+ },
3819
+ scope: {
3971
3820
  type: "string",
3972
- enum: [
3973
- "authorization_code",
3974
- "client_credentials",
3975
- "refresh_token"
3976
- ]
3821
+ description: "Space-separated scopes allowed by the client"
3977
3822
  },
3978
- description: "Requested authentication method for the token endpoint"
3979
- },
3980
- response_types: {
3981
- type: "array",
3982
- items: {
3823
+ user_id: {
3983
3824
  type: "string",
3984
- enum: ["code"]
3825
+ description: "ID of the user who registered the client, null if registered anonymously"
3985
3826
  },
3986
- description: "Requested authentication method for the token endpoint"
3987
- },
3988
- public: {
3989
- type: "boolean",
3990
- description: "Whether the client is public as determined by the type"
3991
- },
3992
- type: {
3993
- type: "string",
3994
- description: "Type of the client",
3995
- enum: [
3996
- "web",
3997
- "native",
3998
- "user-agent-based"
3999
- ]
3827
+ client_id_issued_at: {
3828
+ type: "number",
3829
+ description: "Creation timestamp of this client"
3830
+ },
3831
+ client_name: {
3832
+ type: "string",
3833
+ description: "Name of the OAuth2 application"
3834
+ },
3835
+ client_uri: {
3836
+ type: "string",
3837
+ description: "Name of the OAuth2 application"
3838
+ },
3839
+ logo_uri: {
3840
+ type: "string",
3841
+ description: "Icon URL for the application"
3842
+ },
3843
+ contacts: {
3844
+ type: "array",
3845
+ items: { type: "string" },
3846
+ description: "List representing ways to contact people responsible for this client, typically email addresses"
3847
+ },
3848
+ tos_uri: {
3849
+ type: "string",
3850
+ description: "Client's terms of service uri"
3851
+ },
3852
+ policy_uri: {
3853
+ type: "string",
3854
+ description: "Client's policy uri"
3855
+ },
3856
+ software_id: {
3857
+ type: "string",
3858
+ description: "Unique identifier assigned by the developer to help in the dynamic registration process"
3859
+ },
3860
+ software_version: {
3861
+ type: "string",
3862
+ description: "Version identifier for the software_id"
3863
+ },
3864
+ software_statement: {
3865
+ type: "string",
3866
+ description: "JWT containing metadata values about the client software as claims"
3867
+ },
3868
+ redirect_uris: {
3869
+ type: "array",
3870
+ items: {
3871
+ type: "string",
3872
+ format: "uri"
3873
+ },
3874
+ description: "List of allowed redirect uris"
3875
+ },
3876
+ post_logout_redirect_uris: {
3877
+ type: "array",
3878
+ items: {
3879
+ type: "string",
3880
+ format: "uri"
3881
+ },
3882
+ description: "List of allowed logout redirect uris"
3883
+ },
3884
+ backchannel_logout_uri: {
3885
+ type: "string",
3886
+ format: "uri",
3887
+ description: "RP URL to receive signed Logout Tokens when the end-user's OP session terminates"
3888
+ },
3889
+ backchannel_logout_session_required: {
3890
+ type: "boolean",
3891
+ description: "Whether the RP requires a `sid` claim in every Logout Token"
3892
+ },
3893
+ token_endpoint_auth_method: {
3894
+ type: "string",
3895
+ description: "Requested authentication method for the token endpoint"
3896
+ },
3897
+ grant_types: {
3898
+ type: "array",
3899
+ items: { type: "string" },
3900
+ description: "Grant types the client may use at the token endpoint"
3901
+ },
3902
+ response_types: {
3903
+ type: "array",
3904
+ items: {
3905
+ type: "string",
3906
+ enum: ["code"]
3907
+ },
3908
+ description: "Response types the client may use at the authorization endpoint"
3909
+ },
3910
+ public: {
3911
+ type: "boolean",
3912
+ description: "Whether the client is public as determined by the type"
3913
+ },
3914
+ type: {
3915
+ type: "string",
3916
+ description: "Type of the client",
3917
+ enum: [
3918
+ "web",
3919
+ "native",
3920
+ "user-agent-based"
3921
+ ]
3922
+ },
3923
+ disabled: {
3924
+ type: "boolean",
3925
+ description: "Whether the client is disabled"
3926
+ }
4000
3927
  },
4001
- disabled: {
4002
- type: "boolean",
4003
- description: "Whether the client is disabled"
4004
- }
4005
- },
4006
- required: ["client_id"]
4007
- } } }
4008
- } }
4009
- } }
3928
+ required: ["client_id"]
3929
+ } } }
3930
+ } }
3931
+ }
3932
+ }
4010
3933
  }, async (ctx) => {
4011
3934
  return registerEndpoint(ctx, opts);
4012
3935
  }),
@@ -4023,7 +3946,14 @@ const oauthProvider = (options) => {
4023
3946
  getOAuthConsent: getOAuthConsent(opts),
4024
3947
  getOAuthConsents: getOAuthConsents(opts),
4025
3948
  updateOAuthConsent: updateOAuthConsent(opts),
4026
- deleteOAuthConsent: deleteOAuthConsent(opts)
3949
+ deleteOAuthConsent: deleteOAuthConsent(opts),
3950
+ adminCreateOAuthResource: adminCreateOAuthResource(opts),
3951
+ adminListOAuthResources: adminListOAuthResources(opts),
3952
+ adminGetOAuthResource: adminGetOAuthResource(opts),
3953
+ adminUpdateOAuthResource: adminUpdateOAuthResource(opts),
3954
+ adminDeleteOAuthResource: adminDeleteOAuthResource(opts),
3955
+ adminLinkClientResource: adminLinkClientResource(opts),
3956
+ adminUnlinkClientResource: adminUnlinkClientResource(opts)
4027
3957
  },
4028
3958
  schema: mergeSchema(schema, opts?.schema),
4029
3959
  rateLimit: [
@@ -4063,6 +3993,19 @@ const oauthProvider = (options) => {
4063
3993
  //#endregion
4064
3994
  //#region src/authorize.ts
4065
3995
  /**
3996
+ * Whether a past authentication is still fresh enough for an OIDC `max_age`
3997
+ * request: true when no more than `maxAge` seconds have elapsed since the user
3998
+ * last authenticated. The caller supplies `now`, keeping this pure.
3999
+ */
4000
+ function isWithinMaxAge(sessionCreatedAt, maxAgeSeconds, now) {
4001
+ if (maxAgeSeconds === 0) return false;
4002
+ return now.getTime() - sessionCreatedAt.getTime() <= maxAgeSeconds * 1e3;
4003
+ }
4004
+ function removeMaxAgeFromAuthorizationQuery(query) {
4005
+ const { max_age: _maxAge, ...queryWithoutMaxAge } = query;
4006
+ return queryWithoutMaxAge;
4007
+ }
4008
+ /**
4066
4009
  * Formats an error url. Per OIDC Core 1.0 §5 / RFC 6749 §4.2.2.1, errors on
4067
4010
  * implicit and hybrid flows are delivered in the URL fragment, not the query.
4068
4011
  * Callers on the code flow (default) omit `mode` and get query delivery.
@@ -4209,6 +4152,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
4209
4152
  error_description: "request not found",
4210
4153
  error: "invalid_request"
4211
4154
  });
4155
+ const request = ctx.request;
4212
4156
  let query = ctx.query;
4213
4157
  if (query.request_uri) {
4214
4158
  if (!opts.requestUriResolver) return handleRedirect(ctx, getErrorURL(ctx, "invalid_request_uri", "request_uri not supported"));
@@ -4223,6 +4167,14 @@ async function authorizeEndpoint(ctx, opts, settings) {
4223
4167
  if (urlClientId) query.client_id = urlClientId;
4224
4168
  }
4225
4169
  ctx.query = query;
4170
+ const parsedQuery = authorizationQuerySchema.safeParse(query);
4171
+ if (!parsedQuery.success) return authorizeRedirectOnError(opts)({
4172
+ error: "invalid_request",
4173
+ error_description: "invalid authorization request",
4174
+ ctx
4175
+ });
4176
+ query = parsedQuery.data;
4177
+ ctx.query = query;
4226
4178
  await oAuthState.set({ query: serializeAuthorizationQuery(query).toString() });
4227
4179
  if (!query.client_id) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "client_id is required"));
4228
4180
  if (!query.response_type) return handleRedirect(ctx, getErrorURL(ctx, "invalid_request", "response_type is required"));
@@ -4233,6 +4185,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
4233
4185
  const client = await getClient(ctx, opts, query.client_id);
4234
4186
  if (!client) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "client_id is required"));
4235
4187
  if (client.disabled) return handleRedirect(ctx, getErrorURL(ctx, "client_disabled", "client is disabled"));
4188
+ if (!clientAllowsGrant(client, "authorization_code")) return handleRedirect(ctx, getErrorURL(ctx, "unauthorized_client", "client is not authorized to use the authorization_code grant"));
4236
4189
  if (!findRegisteredRedirectUri(client.redirectUris, query.redirect_uri) || !query.redirect_uri) return handleRedirect(ctx, getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
4237
4190
  let requestedScopes = query.scope?.split(" ").filter((s) => s);
4238
4191
  if (requestedScopes) {
@@ -4246,6 +4199,20 @@ async function authorizeEndpoint(ctx, opts, settings) {
4246
4199
  requestedScopes = client.scopes ?? opts.scopes ?? [];
4247
4200
  query.scope = requestedScopes.join(" ");
4248
4201
  }
4202
+ if (query.resource !== void 0) try {
4203
+ await resolveResourcePolicy(ctx, opts, {
4204
+ resource: query.resource,
4205
+ clientId: client.clientId,
4206
+ requestedScopes
4207
+ });
4208
+ } catch (err) {
4209
+ if (err instanceof APIError$1) {
4210
+ const error = err.body?.error ?? "invalid_target";
4211
+ const description = err.body?.error_description ?? "requested resource invalid";
4212
+ return handleRedirect(ctx, formatErrorURL(query.redirect_uri, error, description, query.state, getIssuer(ctx, opts)));
4213
+ }
4214
+ throw err;
4215
+ }
4249
4216
  const pkceRequired = isPKCERequired(client, requestedScopes);
4250
4217
  if (pkceRequired) {
4251
4218
  if (!query.code_challenge || !query.code_challenge_method) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", pkceRequired.valueOf(), query.state, getIssuer(ctx, opts)));
@@ -4254,18 +4221,22 @@ async function authorizeEndpoint(ctx, opts, settings) {
4254
4221
  if (!query.code_challenge || !query.code_challenge_method) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", "code_challenge and code_challenge_method must both be provided", query.state, getIssuer(ctx, opts)));
4255
4222
  if (!["S256"].includes(query.code_challenge_method)) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method, only S256 is supported", query.state, getIssuer(ctx, opts)));
4256
4223
  }
4257
- const resource = query.resource;
4258
- if (!(await checkResource(ctx, opts, resource, requestedScopes)).success) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_target", "requested resource invalid", query.state, getIssuer(ctx, opts)));
4259
- const requestedResources = toResourceList(resource) ?? [];
4224
+ const requestedResources = toResourceList(query.resource) ?? [];
4260
4225
  const session = await getSessionFromCtx(ctx);
4261
- if (!session || promptSet?.has("login") || promptSet?.has("create")) {
4226
+ const maxAgeSeconds = query.max_age;
4227
+ const hasSatisfiedMaxAge = session != null && maxAgeSeconds !== void 0 && isWithinMaxAge(new Date(session.session.createdAt), maxAgeSeconds, /* @__PURE__ */ new Date());
4228
+ if (!session || session != null && maxAgeSeconds !== void 0 && !hasSatisfiedMaxAge || promptSet?.has("login") || promptSet?.has("create")) {
4262
4229
  if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "login_required", "authentication required");
4263
4230
  return redirectWithPromptCode(ctx, opts, promptSet?.has("create") ? "create" : "login");
4264
4231
  }
4232
+ if (hasSatisfiedMaxAge) {
4233
+ query = removeMaxAgeFromAuthorizationQuery(query);
4234
+ ctx.query = query;
4235
+ }
4265
4236
  if (settings?.isAuthorize && promptSet?.has("select_account")) return redirectWithPromptCode(ctx, opts, "select_account");
4266
4237
  if (settings?.isAuthorize && opts.selectAccount) {
4267
4238
  if (await opts.selectAccount.shouldRedirect({
4268
- headers: ctx.request.headers,
4239
+ headers: request.headers,
4269
4240
  user: session.user,
4270
4241
  session: session.session,
4271
4242
  scopes: requestedScopes
@@ -4276,7 +4247,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
4276
4247
  }
4277
4248
  if (opts.signup?.shouldRedirect) {
4278
4249
  const signupRedirect = await opts.signup.shouldRedirect({
4279
- headers: ctx.request.headers,
4250
+ headers: request.headers,
4280
4251
  user: session.user,
4281
4252
  session: session.session,
4282
4253
  scopes: requestedScopes
@@ -4288,7 +4259,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
4288
4259
  }
4289
4260
  if (!settings?.postLogin && opts.postLogin) {
4290
4261
  if (await opts.postLogin.shouldRedirect({
4291
- headers: ctx.request.headers,
4262
+ headers: request.headers,
4292
4263
  user: session.user,
4293
4264
  session: session.session,
4294
4265
  scopes: requestedScopes
@@ -4402,123 +4373,13 @@ async function signParams(ctx, opts, flags) {
4402
4373
  const params = serializeAuthorizationQuery(ctx.query);
4403
4374
  params.set("exp", String(exp));
4404
4375
  params.set(signedQueryIssuedAtParam, String(issuedAt));
4376
+ params.delete("sig");
4405
4377
  params.delete(postLoginClearedParam);
4406
4378
  if (flags?.postLoginClearedForSession) params.set(postLoginClearedParam, flags.postLoginClearedForSession);
4407
- const signature = await makeSignature(params.toString(), ctx.context.secret);
4408
- params.append("sig", signature);
4379
+ setSignedOAuthQueryParameterNames(params);
4380
+ const signature = await makeSignature(canonicalizeOAuthQueryParams(params).toString(), ctx.context.secret);
4381
+ params.set("sig", signature);
4409
4382
  return params.toString();
4410
4383
  }
4411
4384
  //#endregion
4412
- //#region src/metadata.ts
4413
- function authServerMetadata(ctx, opts, overrides) {
4414
- const baseURL = ctx.context.baseURL;
4415
- return {
4416
- scopes_supported: overrides?.scopes_supported,
4417
- issuer: validateIssuerUrl(opts?.jwt?.issuer ?? baseURL),
4418
- authorization_endpoint: `${baseURL}/oauth2/authorize`,
4419
- token_endpoint: `${baseURL}/oauth2/token`,
4420
- jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
4421
- registration_endpoint: overrides?.dynamic_client_registration_supported ? `${baseURL}/oauth2/register` : void 0,
4422
- introspection_endpoint: `${baseURL}/oauth2/introspect`,
4423
- revocation_endpoint: `${baseURL}/oauth2/revoke`,
4424
- response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
4425
- response_modes_supported: ["query"],
4426
- grant_types_supported: overrides?.grant_types_supported ?? [
4427
- "authorization_code",
4428
- "client_credentials",
4429
- "refresh_token"
4430
- ],
4431
- token_endpoint_auth_methods_supported: [
4432
- ...overrides?.public_client_supported ? ["none"] : [],
4433
- "client_secret_basic",
4434
- "client_secret_post",
4435
- "private_key_jwt"
4436
- ],
4437
- token_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
4438
- introspection_endpoint_auth_methods_supported: [
4439
- "client_secret_basic",
4440
- "client_secret_post",
4441
- "private_key_jwt"
4442
- ],
4443
- introspection_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
4444
- revocation_endpoint_auth_methods_supported: [
4445
- "client_secret_basic",
4446
- "client_secret_post",
4447
- "private_key_jwt"
4448
- ],
4449
- revocation_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
4450
- code_challenge_methods_supported: ["S256"],
4451
- authorization_response_iss_parameter_supported: true
4452
- };
4453
- }
4454
- function oidcServerMetadata(ctx, opts) {
4455
- const baseURL = ctx.context.baseURL;
4456
- const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
4457
- return {
4458
- ...authServerMetadata(ctx, jwtPluginOptions, {
4459
- scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
4460
- dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
4461
- public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
4462
- grant_types_supported: opts.grantTypes,
4463
- jwt_disabled: opts.disableJwtPlugin
4464
- }),
4465
- claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
4466
- userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
4467
- subject_types_supported: opts.pairwiseSecret ? ["public", "pairwise"] : ["public"],
4468
- id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
4469
- end_session_endpoint: `${baseURL}/oauth2/end-session`,
4470
- acr_values_supported: ["urn:mace:incommon:iap:bronze"],
4471
- prompt_values_supported: [
4472
- "login",
4473
- "consent",
4474
- "create",
4475
- "select_account",
4476
- "none"
4477
- ],
4478
- ...mergeDiscoveryMetadata(opts.clientDiscovery)
4479
- };
4480
- }
4481
- const METADATA_CACHE_CONTROL = "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400";
4482
- function metadataResponse(body, extraHeaders) {
4483
- const headers = new Headers(extraHeaders);
4484
- if (!headers.has("Cache-Control")) headers.set("Cache-Control", METADATA_CACHE_CONTROL);
4485
- headers.set("Content-Type", "application/json");
4486
- return new Response(JSON.stringify(body), {
4487
- status: 200,
4488
- headers
4489
- });
4490
- }
4491
- /**
4492
- * Provides an exportable `/.well-known/oauth-authorization-server`.
4493
- *
4494
- * Useful when basePath prevents the endpoint from being located at the root
4495
- * and must be provided manually.
4496
- *
4497
- * @external
4498
- */
4499
- const oauthProviderAuthServerMetadata = (auth, opts) => {
4500
- return async (request) => {
4501
- return metadataResponse(await auth.api.getOAuthServerConfig({
4502
- request,
4503
- asResponse: false
4504
- }), opts?.headers);
4505
- };
4506
- };
4507
- /**
4508
- * Provides an exportable `/.well-known/openid-configuration`.
4509
- *
4510
- * Useful when basePath prevents the endpoint from being located at the root
4511
- * and must be provided manually.
4512
- *
4513
- * @external
4514
- */
4515
- const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
4516
- return async (request) => {
4517
- return metadataResponse(await auth.api.getOpenIdConfig({
4518
- request,
4519
- asResponse: false
4520
- }), opts?.headers);
4521
- };
4522
- };
4523
- //#endregion
4524
- export { authServerMetadata, checkOAuthClient, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata };
4385
+ export { DEFAULT_OAUTH_SCOPES, ResourceUriSchema, authServerMetadata, checkOAuthClient, consumeClientAssertion, extendOAuthProvider, getIssuer, getOAuthProviderApi, getOAuthProviderState, metadataResponse, oauthAuthorizationServerMetadata, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata, raiseResourceServerChallenge };