@better-auth/oauth-provider 1.5.0-beta.9 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,221 +1,58 @@
1
- import { a as getJwtPlugin, c as parseClientMetadata, d as storeToken, f as validateClientCredentials, i as getClient, l as parsePrompt, m as mcpHandler, n as decryptStoredClientSecret, r as deleteFromPrompt, s as getStoredToken, t as basicToClientCredentials, u as storeClientSecret } from "./utils-CUVT0Bep.mjs";
1
+ import { a as getJwtPlugin, c as isPKCERequired, d as storeClientSecret, f as storeToken, h as mcpHandler, i as getClient, l as parseClientMetadata, n as decryptStoredClientSecret, p as validateClientCredentials, r as deleteFromPrompt, s as getStoredToken, t as basicToClientCredentials, u as parsePrompt } from "./utils-D6kv_BUA.mjs";
2
+ import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
2
3
  import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
3
- import { APIError } from "better-call";
4
- import { BetterAuthError } from "@better-auth/core/error";
4
+ import { APIError as APIError$1 } from "better-call";
5
5
  import { constantTimeEqual, generateRandomString, makeSignature } from "better-auth/crypto";
6
6
  import { defineRequestState } from "@better-auth/core/context";
7
7
  import { logger } from "@better-auth/core/env";
8
- import { APIError as APIError$1, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
8
+ import { BetterAuthError } from "@better-auth/core/error";
9
9
  import { parseSetCookieHeader } from "better-auth/cookies";
10
10
  import { mergeSchema } from "better-auth/db";
11
11
  import * as z from "zod";
12
12
  import { signJWT, toExpJWT } from "better-auth/plugins";
13
13
  import { SignJWT, compactVerify, createLocalJWKSet, decodeJwt } from "jose";
14
14
 
15
- //#region src/metadata.ts
16
- function authServerMetadata(ctx, opts, overrides) {
17
- const baseURL = ctx.context.baseURL;
18
- return {
19
- scopes_supported: overrides?.scopes_supported,
20
- issuer: opts?.jwt?.issuer ?? baseURL,
21
- authorization_endpoint: `${baseURL}/oauth2/authorize`,
22
- token_endpoint: `${baseURL}/oauth2/token`,
23
- jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
24
- registration_endpoint: `${baseURL}/oauth2/register`,
25
- introspection_endpoint: `${baseURL}/oauth2/introspect`,
26
- revocation_endpoint: `${baseURL}/oauth2/revoke`,
27
- response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
28
- response_modes_supported: ["query"],
29
- grant_types_supported: overrides?.grant_types_supported ?? [
30
- "authorization_code",
31
- "client_credentials",
32
- "refresh_token"
33
- ],
34
- token_endpoint_auth_methods_supported: [
35
- ...overrides?.public_client_supported ? ["none"] : [],
36
- "client_secret_basic",
37
- "client_secret_post"
38
- ],
39
- introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
40
- revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
41
- code_challenge_methods_supported: ["S256"]
42
- };
43
- }
44
- function oidcServerMetadata(ctx, opts) {
45
- const baseURL = ctx.context.baseURL;
46
- const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
47
- return {
48
- ...authServerMetadata(ctx, jwtPluginOptions, {
49
- scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
50
- public_client_supported: opts.allowUnauthenticatedClientRegistration,
51
- grant_types_supported: opts.grantTypes,
52
- jwt_disabled: opts.disableJwtPlugin
53
- }),
54
- claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
55
- userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
56
- subject_types_supported: ["public"],
57
- id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
58
- end_session_endpoint: `${baseURL}/oauth2/end-session`,
59
- acr_values_supported: ["urn:mace:incommon:iap:bronze"],
60
- prompt_values_supported: [
61
- "login",
62
- "consent",
63
- "create",
64
- "select_account"
65
- ]
66
- };
67
- }
68
- /**
69
- * Provides an exportable `/.well-known/oauth-authorization-server`.
70
- *
71
- * Useful when basePath prevents the endpoint from being located at the root
72
- * and must be provided manually.
73
- *
74
- * @external
75
- */
76
- const oauthProviderAuthServerMetadata = (auth, opts) => {
77
- return async (_request) => {
78
- const res = await auth.api.getOAuthServerConfig();
79
- return new Response(JSON.stringify(res), {
80
- status: 200,
81
- headers: {
82
- "Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
83
- ...opts?.headers,
84
- "Content-Type": "application/json"
85
- }
86
- });
87
- };
88
- };
89
- /**
90
- * Provides an exportable `/.well-known/openid-configuration`.
91
- *
92
- * Useful when basePath prevents the endpoint from being located at the root
93
- * and must be provided manually.
94
- *
95
- * @external
96
- */
97
- const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
98
- return async (_request) => {
99
- const res = await auth.api.getOpenIdConfig();
100
- return new Response(JSON.stringify(res), {
101
- status: 200,
102
- headers: {
103
- "Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
104
- ...opts?.headers,
105
- "Content-Type": "application/json"
106
- }
107
- });
108
- };
109
- };
110
-
111
- //#endregion
112
- //#region src/authorize.ts
113
- /**
114
- * Formats an error url
115
- */
116
- function formatErrorURL(url, error, description, state) {
117
- const searchParams = new URLSearchParams({
118
- error,
119
- error_description: description
120
- });
121
- state && searchParams.append("state", state);
122
- return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
123
- }
124
- const handleRedirect = (ctx, uri) => {
125
- if (ctx.headers?.get("accept")?.includes("application/json")) return {
126
- redirect: true,
127
- url: uri.toString()
128
- };
129
- else throw ctx.redirect(uri);
130
- };
131
- /**
132
- * Error page url if redirect_uri has not been verified yet
133
- * Generates Url for custom error page
134
- */
135
- function getErrorURL(ctx, error, description) {
136
- return formatErrorURL(ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`, error, description);
137
- }
138
- async function authorizeEndpoint(ctx, opts, settings) {
139
- if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError("NOT_FOUND");
140
- if (!ctx.request) throw new APIError("UNAUTHORIZED", {
141
- error_description: "request not found",
15
+ //#region src/consent.ts
16
+ async function consentEndpoint(ctx, opts) {
17
+ const _query = (await oAuthState.get())?.query;
18
+ if (!_query) throw new APIError("BAD_REQUEST", {
19
+ error_description: "missing oauth query",
142
20
  error: "invalid_request"
143
21
  });
144
- const query = ctx.query;
145
- if (!query.client_id) throw ctx.redirect(getErrorURL(ctx, "invalid_client", "client_id is required"));
146
- if (!query.response_type) throw ctx.redirect(getErrorURL(ctx, "invalid_request", "response_type is required"));
147
- const promptSet = ctx.query?.prompt ? parsePrompt(ctx.query?.prompt) : void 0;
148
- if (promptSet?.has("select_account") && !opts.selectAccount?.page) throw ctx.redirect(getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
149
- if (!(query.response_type === "code")) throw ctx.redirect(getErrorURL(ctx, "unsupported_response_type", "unsupported response type"));
150
- const client = await getClient(ctx, opts, query.client_id);
151
- if (!client) throw ctx.redirect(getErrorURL(ctx, "invalid_client", "client_id is required"));
152
- if (client.disabled) throw ctx.redirect(getErrorURL(ctx, "client_disabled", "client is disabled"));
153
- if (!client.redirectUris?.find((url) => url === query.redirect_uri) || !query.redirect_uri) throw ctx.redirect(getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
154
- let requestedScopes = query.scope?.split(" ").filter((s) => s);
22
+ const query = new URLSearchParams(_query);
23
+ const originalRequestedScopes = query.get("scope")?.split(" ") ?? [];
24
+ const clientId = query.get("client_id");
25
+ if (!clientId) throw new APIError("BAD_REQUEST", {
26
+ error_description: "client_id is required",
27
+ error: "invalid_client"
28
+ });
29
+ const requestedScopes = ctx.body.scope?.split(" ");
155
30
  if (requestedScopes) {
156
- const validScopes = new Set(client.scopes ?? opts.scopes);
157
- const invalidScopes = requestedScopes.filter((scope) => {
158
- return !validScopes?.has(scope) || scope === "offline_access" && (query.code_challenge_method !== "S256" || !query.code_challenge);
31
+ if (!requestedScopes.every((sc) => originalRequestedScopes?.includes(sc))) throw new APIError("BAD_REQUEST", {
32
+ error_description: "Scope not originally requested",
33
+ error: "invalid_request"
159
34
  });
160
- if (invalidScopes.length) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`, query.state));
161
- }
162
- if (!requestedScopes) {
163
- requestedScopes = client.scopes ?? opts.scopes ?? [];
164
- query.scope = requestedScopes.join(" ");
165
35
  }
166
- if (!query.code_challenge || !query.code_challenge_method) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", "pkce is required", query.state));
167
- if (!["S256"].includes(query.code_challenge_method)) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method", query.state));
36
+ if (!(ctx.body.accept === true)) return {
37
+ redirect: true,
38
+ url: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0, getIssuer(ctx, opts))
39
+ };
168
40
  const session = await getSessionFromCtx(ctx);
169
- if (!session || promptSet?.has("login") || promptSet?.has("create")) return redirectWithPromptCode(ctx, opts, promptSet?.has("create") ? "create" : "login");
170
- if (settings?.isAuthorize && promptSet?.has("select_account")) return redirectWithPromptCode(ctx, opts, "select_account");
171
- if (settings?.isAuthorize && opts.selectAccount) {
172
- if (await opts.selectAccount.shouldRedirect({
173
- headers: ctx.request.headers,
174
- user: session.user,
175
- session: session.session,
176
- scopes: requestedScopes
177
- })) return redirectWithPromptCode(ctx, opts, "select_account");
178
- }
179
- if (opts.signup?.shouldRedirect) {
180
- const signupRedirect = await opts.signup.shouldRedirect({
181
- headers: ctx.request.headers,
182
- user: session.user,
183
- session: session.session,
184
- scopes: requestedScopes
185
- });
186
- if (signupRedirect) return redirectWithPromptCode(ctx, opts, "create", typeof signupRedirect === "string" ? signupRedirect : void 0);
187
- }
188
- if (!settings?.postLogin && opts.postLogin) {
189
- if (await opts.postLogin.shouldRedirect({
190
- headers: ctx.request.headers,
191
- user: session.user,
192
- session: session.session,
193
- scopes: requestedScopes
194
- })) return redirectWithPromptCode(ctx, opts, "post_login");
195
- }
196
- if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent");
197
41
  const referenceId = await opts.postLogin?.consentReferenceId?.({
198
- user: session.user,
199
- session: session.session,
200
- scopes: requestedScopes
201
- });
202
- if (client.skipConsent) return redirectWithAuthorizationCode(ctx, opts, {
203
- query,
204
- clientId: client.clientId,
205
- userId: session.user.id,
206
- sessionId: session.session.id,
207
- referenceId
42
+ user: session?.user,
43
+ session: session?.session,
44
+ scopes: requestedScopes ?? originalRequestedScopes
208
45
  });
209
- const consent = await ctx.context.adapter.findOne({
46
+ const foundConsent = await ctx.context.adapter.findOne({
210
47
  model: "oauthConsent",
211
48
  where: [
212
49
  {
213
50
  field: "clientId",
214
- value: client.clientId
51
+ value: clientId
215
52
  },
216
53
  {
217
54
  field: "userId",
218
- value: session.user.id
55
+ value: session?.user.id
219
56
  },
220
57
  ...referenceId ? [{
221
58
  field: "referenceId",
@@ -223,142 +60,40 @@ async function authorizeEndpoint(ctx, opts, settings) {
223
60
  }] : []
224
61
  ]
225
62
  });
226
- if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val))) return redirectWithPromptCode(ctx, opts, "consent");
227
- return redirectWithAuthorizationCode(ctx, opts, {
228
- query,
229
- clientId: client.clientId,
230
- userId: session.user.id,
231
- sessionId: session.session.id,
232
- referenceId
233
- });
234
- }
235
- async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
236
- const code = generateRandomString(32, "a-z", "A-Z", "0-9");
237
63
  const iat = Math.floor(Date.now() / 1e3);
238
- const exp = iat + (opts.codeExpiresIn ?? 600);
239
- const data = {
240
- identifier: await storeToken(opts.storeTokens, code, "authorization_code"),
64
+ const consent = {
65
+ clientId,
66
+ userId: session?.user.id,
67
+ scopes: requestedScopes ?? originalRequestedScopes,
68
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3),
241
69
  updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
242
- expiresAt: /* @__PURE__ */ new Date(exp * 1e3),
243
- value: JSON.stringify({
244
- type: "authorization_code",
245
- query: ctx.query,
246
- userId: verificationValue.userId,
247
- sessionId: verificationValue?.sessionId,
248
- referenceId: verificationValue.referenceId
249
- })
70
+ referenceId
250
71
  };
251
- ctx.context.verification_id ? await ctx.context.internalAdapter.updateVerificationValue(ctx.context.verification_id, data) : await ctx.context.internalAdapter.createVerificationValue({
252
- ...data,
253
- createdAt: /* @__PURE__ */ new Date(iat * 1e3)
254
- });
255
- const redirectUriWithCode = new URL(verificationValue.query.redirect_uri);
256
- redirectUriWithCode.searchParams.set("code", code);
257
- if (verificationValue.query.state) redirectUriWithCode.searchParams.set("state", verificationValue.query.state);
258
- return handleRedirect(ctx, redirectUriWithCode.toString());
259
- }
260
- async function redirectWithPromptCode(ctx, opts, type, page) {
261
- const queryParams = await signParams(ctx, opts);
262
- let path = opts.loginPage;
263
- if (type === "select_account") path = opts.selectAccount?.page ?? opts.loginPage;
264
- else if (type === "post_login") {
265
- if (!opts.postLogin?.page) throw new APIError("INTERNAL_SERVER_ERROR", { error_description: "postLogin should have been defined" });
266
- path = opts.postLogin?.page;
267
- } else if (type === "consent") path = opts.consentPage;
268
- else if (type === "create") path = opts.signup?.page ?? opts.loginPage;
269
- return handleRedirect(ctx, `${page ?? path}?${queryParams}`);
270
- }
271
- async function signParams(ctx, opts) {
272
- const exp = Math.floor(Date.now() / 1e3) + (opts.codeExpiresIn ?? 600);
273
- const params = new URLSearchParams(ctx.query);
274
- params.set("exp", String(exp));
275
- const signature = await makeSignature(params.toString(), ctx.context.secret);
276
- params.append("sig", signature);
277
- return params.toString();
278
- }
279
-
280
- //#endregion
281
- //#region src/consent.ts
282
- async function consentEndpoint(ctx, opts) {
283
- const _query = (await oAuthState.get())?.query;
284
- if (!_query) throw new APIError$1("BAD_REQUEST", {
285
- error_description: "missing oauth query",
286
- error: "invalid_request"
287
- });
288
- const query = new URLSearchParams(_query);
289
- const originalRequestedScopes = query.get("scope")?.split(" ") ?? [];
290
- const clientId = query.get("client_id");
291
- if (!clientId) throw new APIError$1("BAD_REQUEST", {
292
- error_description: "client_id is required",
293
- error: "invalid_client"
294
- });
295
- const requestedScopes = ctx.body.scope?.split(" ");
296
- if (requestedScopes) {
297
- if (!requestedScopes.every((sc) => originalRequestedScopes?.includes(sc))) throw new APIError$1("BAD_REQUEST", {
298
- error_description: "Scope not originally requested",
299
- error: "invalid_request"
300
- });
301
- }
302
- if (!(ctx.body.accept === true)) return {
303
- redirect: true,
304
- uri: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0)
305
- };
306
- const session = await getSessionFromCtx(ctx);
307
- const referenceId = await opts.postLogin?.consentReferenceId?.({
308
- user: session?.user,
309
- session: session?.session,
310
- scopes: requestedScopes ?? originalRequestedScopes
311
- });
312
- const foundConsent = await ctx.context.adapter.findOne({
313
- model: "oauthConsent",
314
- where: [
315
- {
316
- field: "clientId",
317
- value: clientId
318
- },
319
- {
320
- field: "userId",
321
- value: session?.user.id
322
- },
323
- ...referenceId ? [{
324
- field: "referenceId",
325
- value: referenceId
326
- }] : []
327
- ]
328
- });
329
- const iat = Math.floor(Date.now() / 1e3);
330
- const consent = {
331
- clientId,
332
- userId: session?.user.id,
333
- scopes: requestedScopes ?? originalRequestedScopes,
334
- createdAt: /* @__PURE__ */ new Date(iat * 1e3),
335
- updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
336
- referenceId
337
- };
338
- foundConsent?.id ? await ctx.context.adapter.update({
339
- model: "oauthConsent",
340
- where: [{
341
- field: "id",
342
- value: foundConsent.id
343
- }],
344
- update: {
345
- scopes: consent.scopes,
346
- updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
347
- }
348
- }) : await ctx.context.adapter.create({
349
- model: "oauthConsent",
350
- data: {
351
- ...consent,
352
- scopes: consent.scopes
353
- }
72
+ foundConsent?.id ? await ctx.context.adapter.update({
73
+ model: "oauthConsent",
74
+ where: [{
75
+ field: "id",
76
+ value: foundConsent.id
77
+ }],
78
+ update: {
79
+ scopes: consent.scopes,
80
+ updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
81
+ }
82
+ }) : await ctx.context.adapter.create({
83
+ model: "oauthConsent",
84
+ data: {
85
+ ...consent,
86
+ scopes: consent.scopes
87
+ }
354
88
  });
89
+ if (requestedScopes) query.set("scope", consent.scopes.join(" "));
355
90
  ctx?.headers?.set("accept", "application/json");
356
91
  ctx.query = deleteFromPrompt(query, "consent");
357
92
  ctx.context.postLogin = true;
358
93
  const { url } = await authorizeEndpoint(ctx, opts);
359
94
  return {
360
95
  redirect: true,
361
- uri: url
96
+ url
362
97
  };
363
98
  }
364
99
 
@@ -368,14 +103,14 @@ async function continueEndpoint(ctx, opts) {
368
103
  if (ctx.body.selected === true) return await selected(ctx, opts);
369
104
  else if (ctx.body.created === true) return await created(ctx, opts);
370
105
  else if (ctx.body.postLogin === true) return await postLogin(ctx, opts);
371
- else throw new APIError$1("BAD_REQUEST", {
106
+ else throw new APIError("BAD_REQUEST", {
372
107
  error_description: "Missing parameters",
373
108
  error: "invalid_request"
374
109
  });
375
110
  }
376
111
  async function selected(ctx, opts) {
377
112
  const _query = (await oAuthState.get())?.query;
378
- if (!_query) throw new APIError$1("BAD_REQUEST", {
113
+ if (!_query) throw new APIError("BAD_REQUEST", {
379
114
  error_description: "missing oauth query",
380
115
  error: "invalid_request"
381
116
  });
@@ -384,12 +119,12 @@ async function selected(ctx, opts) {
384
119
  const { url } = await authorizeEndpoint(ctx, opts);
385
120
  return {
386
121
  redirect: true,
387
- uri: url
122
+ url
388
123
  };
389
124
  }
390
125
  async function created(ctx, opts) {
391
126
  const _query = (await oAuthState.get())?.query;
392
- if (!_query) throw new APIError$1("BAD_REQUEST", {
127
+ if (!_query) throw new APIError("BAD_REQUEST", {
393
128
  error_description: "missing oauth query",
394
129
  error: "invalid_request"
395
130
  });
@@ -397,12 +132,12 @@ async function created(ctx, opts) {
397
132
  const { url } = await authorizeEndpoint(ctx, opts);
398
133
  return {
399
134
  redirect: true,
400
- uri: url
135
+ url
401
136
  };
402
137
  }
403
138
  async function postLogin(ctx, opts) {
404
139
  const _query = (await oAuthState.get())?.query;
405
- if (!_query) throw new APIError$1("BAD_REQUEST", {
140
+ if (!_query) throw new APIError("BAD_REQUEST", {
406
141
  error_description: "missing oauth query",
407
142
  error: "invalid_request"
408
143
  });
@@ -412,7 +147,7 @@ async function postLogin(ctx, opts) {
412
147
  const { url } = await authorizeEndpoint(ctx, opts, { postLogin: true });
413
148
  return {
414
149
  redirect: true,
415
- uri: url
150
+ url
416
151
  };
417
152
  }
418
153
 
@@ -445,28 +180,28 @@ function userNormalClaims(user, scopes) {
445
180
  * Handles the /oauth2/userinfo endpoint
446
181
  */
447
182
  async function userInfoEndpoint(ctx, opts) {
448
- if (!ctx.request) throw new APIError$1("UNAUTHORIZED", {
183
+ if (!ctx.request) throw new APIError("UNAUTHORIZED", {
449
184
  error_description: "request not found",
450
185
  error: "invalid_request"
451
186
  });
452
187
  const authorization = ctx.request.headers.get("authorization");
453
188
  const token = typeof authorization === "string" && authorization?.startsWith("Bearer ") ? authorization?.replace("Bearer ", "") : authorization;
454
- if (!token?.length) throw new APIError$1("UNAUTHORIZED", {
189
+ if (!token?.length) throw new APIError("UNAUTHORIZED", {
455
190
  error_description: "authorization header not found",
456
191
  error: "invalid_request"
457
192
  });
458
193
  const jwt = await validateAccessToken(ctx, opts, token);
459
194
  const scopes = jwt.scope?.split(" ");
460
- if (!scopes?.includes("openid")) throw new APIError$1("BAD_REQUEST", {
195
+ if (!scopes?.includes("openid")) throw new APIError("BAD_REQUEST", {
461
196
  error_description: "Missing required scope",
462
197
  error: "invalid_scope"
463
198
  });
464
- if (!jwt.sub) throw new APIError$1("BAD_REQUEST", {
199
+ if (!jwt.sub) throw new APIError("BAD_REQUEST", {
465
200
  error_description: "user not found",
466
201
  error: "invalid_request"
467
202
  });
468
203
  const user = await ctx.context.internalAdapter.findUserById(jwt.sub);
469
- if (!user) throw new APIError$1("BAD_REQUEST", {
204
+ if (!user) throw new APIError("BAD_REQUEST", {
470
205
  error_description: "user not found",
471
206
  error: "invalid_request"
472
207
  });
@@ -490,7 +225,7 @@ async function userInfoEndpoint(ctx, opts) {
490
225
  */
491
226
  async function tokenEndpoint(ctx, opts) {
492
227
  const grantType = ctx.body?.grant_type;
493
- if (opts.grantTypes && grantType && !opts.grantTypes.includes(grantType)) throw new APIError$1("BAD_REQUEST", {
228
+ if (opts.grantTypes && grantType && !opts.grantTypes.includes(grantType)) throw new APIError("BAD_REQUEST", {
494
229
  error_description: `unsupported grant_type ${grantType}`,
495
230
  error: "unsupported_grant_type"
496
231
  });
@@ -498,11 +233,11 @@ async function tokenEndpoint(ctx, opts) {
498
233
  case "authorization_code": return handleAuthorizationCodeGrant(ctx, opts);
499
234
  case "client_credentials": return handleClientCredentialsGrant(ctx, opts);
500
235
  case "refresh_token": return handleRefreshTokenGrant(ctx, opts);
501
- case void 0: throw new APIError$1("BAD_REQUEST", {
236
+ case void 0: throw new APIError("BAD_REQUEST", {
502
237
  error_description: "missing required grant_type",
503
238
  error: "unsupported_grant_type"
504
239
  });
505
- default: throw new APIError$1("BAD_REQUEST", {
240
+ default: throw new APIError("BAD_REQUEST", {
506
241
  error_description: `unsupported grant_type ${grantType}`,
507
242
  error: "unsupported_grant_type"
508
243
  });
@@ -538,11 +273,11 @@ async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, r
538
273
  * Creates a user id token in code_authorization with scope of 'openid'
539
274
  * and hybrid/implicit (not yet implemented) flows
540
275
  */
541
- async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId) {
276
+ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime) {
542
277
  const iat = Math.floor(Date.now() / 1e3);
543
278
  const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
544
279
  const userClaims = userNormalClaims(user, scopes);
545
- const authTime = Math.floor((ctx.context.session?.session.createdAt ?? /* @__PURE__ */ new Date(iat * 1e3)).getTime() / 1e3);
280
+ const authTimeSec = authTime != null ? Math.floor(authTime.getTime() / 1e3) : void 0;
546
281
  const acr = "urn:mace:incommon:iap:bronze";
547
282
  const customClaims = opts.customIdTokenClaims ? await opts.customIdTokenClaims({
548
283
  user,
@@ -553,7 +288,7 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId)
553
288
  const payload = {
554
289
  ...customClaims,
555
290
  ...userClaims,
556
- auth_time: authTime,
291
+ auth_time: authTimeSec,
557
292
  acr,
558
293
  iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
559
294
  sub: user.id,
@@ -582,7 +317,7 @@ async function encodeRefreshToken(opts, token, sessionId) {
582
317
  */
583
318
  async function decodeRefreshToken(opts, token) {
584
319
  if (opts.prefix?.refreshToken) if (token.startsWith(opts.prefix.refreshToken)) token = token.replace(opts.prefix.refreshToken, "");
585
- else throw new APIError$1("BAD_REQUEST", {
320
+ else throw new APIError("BAD_REQUEST", {
586
321
  error_description: "refresh token not found",
587
322
  error: "invalid_token"
588
323
  });
@@ -608,7 +343,7 @@ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload,
608
343
  });
609
344
  return (opts.prefix?.opaqueAccessToken ?? "") + token;
610
345
  }
611
- async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, payload, originalRefresh) {
346
+ async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, payload, originalRefresh, authTime) {
612
347
  const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
613
348
  const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
614
349
  const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
@@ -630,6 +365,7 @@ async function createRefreshToken(ctx, opts, user, referenceId, client, scopes,
630
365
  sessionId,
631
366
  userId: user.id,
632
367
  referenceId,
368
+ authTime,
633
369
  scopes,
634
370
  createdAt: /* @__PURE__ */ new Date(iat * 1e3),
635
371
  expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
@@ -648,14 +384,14 @@ async function checkResource(ctx, opts, scopes) {
648
384
  if (audience) {
649
385
  if (scopes.includes("openid")) audience.push(`${ctx.context.baseURL}/oauth2/userinfo`);
650
386
  const validAudiences = new Set([...opts.validAudiences ?? [ctx.context.baseURL], scopes?.includes("openid") ? `${ctx.context.baseURL}/oauth2/userinfo` : void 0].flat().filter((v) => v?.length));
651
- for (const aud of audience) if (!validAudiences.has(aud)) throw new APIError$1("BAD_REQUEST", {
387
+ for (const aud of audience) if (!validAudiences.has(aud)) throw new APIError("BAD_REQUEST", {
652
388
  error_description: "requested resource invalid",
653
389
  error: "invalid_request"
654
390
  });
655
391
  }
656
392
  return audience?.length === 1 ? audience.at(0) : audience;
657
393
  }
658
- async function createUserTokens(ctx, opts, client, scopes, user, referenceId, sessionId, nonce, additional) {
394
+ async function createUserTokens(ctx, opts, client, scopes, user, referenceId, sessionId, nonce, additional, authTime) {
659
395
  const iat = Math.floor(Date.now() / 1e3);
660
396
  const defaultExp = iat + (opts.accessTokenExpiresIn ?? 3600);
661
397
  const exp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
@@ -669,7 +405,7 @@ async function createUserTokens(ctx, opts, client, scopes, user, referenceId, se
669
405
  iat,
670
406
  exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
671
407
  sid: sessionId
672
- }, additional?.refreshToken) : void 0;
408
+ }, additional?.refreshToken, authTime) : void 0;
673
409
  const [accessToken, refreshToken, idToken] = await Promise.all([
674
410
  isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, {
675
411
  iat,
@@ -684,8 +420,8 @@ async function createUserTokens(ctx, opts, client, scopes, user, referenceId, se
684
420
  iat,
685
421
  exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
686
422
  sid: sessionId
687
- }, additional?.refreshToken) : void 0,
688
- isIdToken ? createIdToken(ctx, opts, user, client, scopes, nonce, sessionId) : void 0
423
+ }, additional?.refreshToken, authTime) : void 0,
424
+ isIdToken ? createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime) : void 0
689
425
  ]);
690
426
  return ctx.json({
691
427
  access_token: accessToken,
@@ -704,32 +440,32 @@ async function createUserTokens(ctx, opts, client, scopes, user, referenceId, se
704
440
  async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri) {
705
441
  const verification = await ctx.context.internalAdapter.findVerificationValue(await storeToken(opts.storeTokens, code, "authorization_code"));
706
442
  const verificationValue = verification ? JSON.parse(verification?.value) : void 0;
707
- if (!verification) throw new APIError$1("UNAUTHORIZED", {
443
+ if (!verification) throw new APIError("UNAUTHORIZED", {
708
444
  error_description: "Invalid code",
709
445
  error: "invalid_verification"
710
446
  });
711
447
  if (verification?.id) await ctx.context.internalAdapter.deleteVerificationValue(verification.id);
712
- if (!verification.expiresAt || verification.expiresAt < /* @__PURE__ */ new Date()) throw new APIError$1("UNAUTHORIZED", {
448
+ if (!verification.expiresAt || verification.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("UNAUTHORIZED", {
713
449
  error_description: "code expired",
714
450
  error: "invalid_verification"
715
451
  });
716
- if (!verificationValue) throw new APIError$1("UNAUTHORIZED", {
452
+ if (!verificationValue) throw new APIError("UNAUTHORIZED", {
717
453
  error_description: "missing verification value content",
718
454
  error: "invalid_verification"
719
455
  });
720
- if (verificationValue.type !== "authorization_code") throw new APIError$1("UNAUTHORIZED", {
456
+ if (verificationValue.type !== "authorization_code") throw new APIError("UNAUTHORIZED", {
721
457
  error_description: "incorrect verification type",
722
458
  error: "invalid_verification"
723
459
  });
724
- if (verificationValue.query.client_id !== client_id) throw new APIError$1("UNAUTHORIZED", {
460
+ if (verificationValue.query.client_id !== client_id) throw new APIError("UNAUTHORIZED", {
725
461
  error_description: "invalid client_id",
726
462
  error: "invalid_client"
727
463
  });
728
- if (!verificationValue.userId) throw new APIError$1("BAD_REQUEST", {
464
+ if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
729
465
  error_description: "missing user_id on challenge",
730
466
  error: "invalid_user"
731
467
  });
732
- if (verificationValue.query?.redirect_uri && verificationValue.query?.redirect_uri !== redirect_uri) throw new APIError$1("BAD_REQUEST", {
468
+ if (verificationValue.query?.redirect_uri && verificationValue.query?.redirect_uri !== redirect_uri) throw new APIError("BAD_REQUEST", {
733
469
  error_description: "missing verification redirect_uri",
734
470
  error: "invalid_request"
735
471
  });
@@ -746,50 +482,66 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
746
482
  client_id = res?.client_id;
747
483
  client_secret = res?.client_secret;
748
484
  }
749
- if (!client_id) throw new APIError$1("BAD_REQUEST", {
485
+ if (!client_id) throw new APIError("BAD_REQUEST", {
750
486
  error_description: "client_id is required",
751
487
  error: "invalid_request"
752
488
  });
753
- if (!code) throw new APIError$1("BAD_REQUEST", {
489
+ if (!code) throw new APIError("BAD_REQUEST", {
754
490
  error_description: "code is required",
755
491
  error: "invalid_request"
756
492
  });
757
- if (!redirect_uri) throw new APIError$1("BAD_REQUEST", {
493
+ if (!redirect_uri) throw new APIError("BAD_REQUEST", {
758
494
  error_description: "redirect_uri is required",
759
495
  error: "invalid_request"
760
496
  });
761
497
  const isAuthCodeWithSecret = client_id && client_secret;
762
498
  const isAuthCodeWithPkce = client_id && code && code_verifier;
763
- if (!(isAuthCodeWithPkce || isAuthCodeWithSecret)) throw new APIError$1("BAD_REQUEST", {
764
- error_description: "Missing a required credential value for authorization_code grant",
499
+ if (!isAuthCodeWithSecret && !isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
500
+ error_description: "Either code_verifier or client_secret is required",
765
501
  error: "invalid_request"
766
502
  });
767
503
  /** Get and check Verification Value */
768
504
  const verificationValue = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri);
769
505
  const scopes = verificationValue.query.scope?.split(" ");
770
- if (!scopes) throw new APIError$1("INTERNAL_SERVER_ERROR", {
506
+ if (!scopes) throw new APIError("INTERNAL_SERVER_ERROR", {
771
507
  error_description: "verification scope unset",
772
508
  error: "invalid_scope"
773
509
  });
774
510
  /** Verify Client */
775
511
  const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes);
776
- /** Check challenge */
777
- const challenge = code_verifier && verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0;
778
- if (isAuthCodeWithSecret && (challenge || verificationValue?.query?.code_challenge) && challenge !== verificationValue.query?.code_challenge) throw new APIError$1("UNAUTHORIZED", {
779
- error_description: "code verification failed",
780
- error: "invalid_request"
781
- });
782
- if (isAuthCodeWithPkce && challenge !== verificationValue.query?.code_challenge) throw new APIError$1("UNAUTHORIZED", {
783
- error_description: "code verification failed",
512
+ if (isPKCERequired(client, (verificationValue.query?.scope)?.split(" ") || [])) {
513
+ if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
514
+ error_description: "PKCE is required for this client",
515
+ error: "invalid_request"
516
+ });
517
+ } else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret)) throw new APIError("BAD_REQUEST", {
518
+ error_description: "Either PKCE (code_verifier) or client authentication (client_secret) is required",
784
519
  error: "invalid_request"
785
520
  });
521
+ /** Check PKCE challenge if verifier is provided */
522
+ const pkceUsedInAuth = !!verificationValue.query?.code_challenge;
523
+ const pkceUsedInToken = !!code_verifier;
524
+ if (pkceUsedInAuth || pkceUsedInToken) {
525
+ if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
526
+ error_description: "code_verifier required because PKCE was used in authorization",
527
+ error: "invalid_request"
528
+ });
529
+ if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
530
+ error_description: "code_verifier provided but PKCE was not used in authorization",
531
+ error: "invalid_request"
532
+ });
533
+ if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("UNAUTHORIZED", {
534
+ error_description: "code verification failed",
535
+ error: "invalid_request"
536
+ });
537
+ }
786
538
  /** Get user */
787
- if (!verificationValue.userId) throw new APIError$1("BAD_REQUEST", {
539
+ if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
788
540
  error_description: "missing user, user may have been deleted",
789
541
  error: "invalid_user"
790
542
  });
791
543
  const user = await ctx.context.internalAdapter.findUserById(verificationValue.userId);
792
- if (!user) throw new APIError$1("BAD_REQUEST", {
544
+ if (!user) throw new APIError("BAD_REQUEST", {
793
545
  error_description: "missing user, user may have been deleted",
794
546
  error: "invalid_user"
795
547
  });
@@ -800,11 +552,12 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
800
552
  value: verificationValue.sessionId
801
553
  }]
802
554
  });
803
- if (!session || session.expiresAt < /* @__PURE__ */ new Date()) throw new APIError$1("BAD_REQUEST", {
555
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
804
556
  error_description: "session no longer exists",
805
557
  error: "invalid_request"
806
558
  });
807
- return createUserTokens(ctx, opts, client, verificationValue.query.scope?.split(" ") ?? [], user, verificationValue.referenceId, session.id, verificationValue.query?.nonce);
559
+ const authTime = verificationValue.authTime != null ? new Date(verificationValue.authTime) : new Date(session.createdAt);
560
+ return createUserTokens(ctx, opts, client, verificationValue.query.scope?.split(" ") ?? [], user, verificationValue.referenceId, session.id, verificationValue.query?.nonce, void 0, authTime);
808
561
  }
809
562
  /**
810
563
  * Grant that allows direct access to an API using the application's credentials
@@ -820,11 +573,11 @@ async function handleClientCredentialsGrant(ctx, opts) {
820
573
  client_id = res?.client_id;
821
574
  client_secret = res?.client_secret;
822
575
  }
823
- if (!client_id) throw new APIError$1("BAD_REQUEST", {
576
+ if (!client_id) throw new APIError("BAD_REQUEST", {
824
577
  error_description: "Missing required client_id",
825
578
  error: "invalid_grant"
826
579
  });
827
- if (!client_secret) throw new APIError$1("BAD_REQUEST", {
580
+ if (!client_secret) throw new APIError("BAD_REQUEST", {
828
581
  error_description: "Missing a required client_secret",
829
582
  error: "invalid_grant"
830
583
  });
@@ -838,10 +591,10 @@ async function handleClientCredentialsGrant(ctx, opts) {
838
591
  "email",
839
592
  "offline_access"
840
593
  ]);
841
- const invalidScopes = requestedScopes.filter((scope$1) => {
842
- return !validScopes?.has(scope$1) || oidcScopes.has(scope$1);
594
+ const invalidScopes = requestedScopes.filter((scope) => {
595
+ return !validScopes?.has(scope) || oidcScopes.has(scope);
843
596
  });
844
- if (invalidScopes.length) throw new APIError$1("BAD_REQUEST", {
597
+ if (invalidScopes.length) throw new APIError("BAD_REQUEST", {
845
598
  error_description: `The following scopes are invalid: ${invalidScopes.join(", ")}`,
846
599
  error: "invalid_scope"
847
600
  });
@@ -899,11 +652,11 @@ async function handleRefreshTokenGrant(ctx, opts) {
899
652
  client_id = res?.client_id;
900
653
  client_secret = res?.client_secret;
901
654
  }
902
- if (!client_id) throw new APIError$1("BAD_REQUEST", {
655
+ if (!client_id) throw new APIError("BAD_REQUEST", {
903
656
  error_description: "Missing required client_id",
904
657
  error: "invalid_grant"
905
658
  });
906
- if (!refresh_token) throw new APIError$1("BAD_REQUEST", {
659
+ if (!refresh_token) throw new APIError("BAD_REQUEST", {
907
660
  error_description: "Missing a required refresh_token for refresh_token grant",
908
661
  error: "invalid_grant"
909
662
  });
@@ -915,17 +668,17 @@ async function handleRefreshTokenGrant(ctx, opts) {
915
668
  value: await getStoredToken(opts.storeTokens, decodedRefresh.token, "refresh_token")
916
669
  }]
917
670
  });
918
- if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
671
+ if (!refreshToken) throw new APIError("BAD_REQUEST", {
919
672
  error_description: "session not found",
920
- error: "invalid_request"
673
+ error: "invalid_grant"
921
674
  });
922
- if (refreshToken.clientId !== client_id) throw new APIError$1("BAD_REQUEST", {
675
+ if (refreshToken.clientId !== client_id) throw new APIError("BAD_REQUEST", {
923
676
  error_description: "invalid client_id",
924
677
  error: "invalid_client"
925
678
  });
926
- if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) throw new APIError$1("BAD_REQUEST", {
679
+ if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
927
680
  error_description: "invalid refresh token",
928
- error: "invalid_request"
681
+ error: "invalid_grant"
929
682
  });
930
683
  if (refreshToken.revoked) {
931
684
  await ctx.context.adapter.deleteMany({
@@ -938,27 +691,28 @@ async function handleRefreshTokenGrant(ctx, opts) {
938
691
  value: refreshToken.userId
939
692
  }]
940
693
  });
941
- throw new APIError$1("BAD_REQUEST", {
694
+ throw new APIError("BAD_REQUEST", {
942
695
  error_description: "invalid refresh token",
943
- error: "invalid_request"
696
+ error: "invalid_grant"
944
697
  });
945
698
  }
946
699
  const scopes = refreshToken?.scopes;
947
700
  const requestedScopes = scope?.split(" ");
948
701
  if (requestedScopes) {
949
702
  const validScopes = new Set(scopes);
950
- for (const requestedScope of requestedScopes) if (!validScopes.has(requestedScope)) throw new APIError$1("BAD_REQUEST", {
703
+ for (const requestedScope of requestedScopes) if (!validScopes.has(requestedScope)) throw new APIError("BAD_REQUEST", {
951
704
  error_description: `unable to issue scope ${requestedScope}`,
952
705
  error: "invalid_scope"
953
706
  });
954
707
  }
955
708
  const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes);
956
709
  const user = await ctx.context.internalAdapter.findUserById(refreshToken.userId);
957
- if (!user) throw new APIError$1("BAD_REQUEST", {
710
+ if (!user) throw new APIError("BAD_REQUEST", {
958
711
  error_description: "user not found",
959
712
  error: "invalid_request"
960
713
  });
961
- return createUserTokens(ctx, opts, client, requestedScopes ?? scopes, user, refreshToken.referenceId, refreshToken.sessionId, void 0, { refreshToken });
714
+ const authTime = refreshToken.authTime != null ? new Date(refreshToken.authTime) : void 0;
715
+ return createUserTokens(ctx, opts, client, requestedScopes ?? scopes, user, refreshToken.referenceId, refreshToken.sessionId, void 0, { refreshToken }, authTime);
962
716
  }
963
717
 
964
718
  //#endregion
@@ -991,7 +745,7 @@ async function validateJwtAccessToken(ctx, opts, token, clientId) {
991
745
  });
992
746
  } catch (error) {
993
747
  if (error instanceof Error) {
994
- if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError("BAD_REQUEST", {
748
+ if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
995
749
  error_description: "invalid JWT signature",
996
750
  error: "invalid_request"
997
751
  });
@@ -1030,7 +784,7 @@ async function validateJwtAccessToken(ctx, opts, token, clientId) {
1030
784
  async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
1031
785
  let tokenValue = token;
1032
786
  if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
1033
- else throw new APIError("BAD_REQUEST", {
787
+ else throw new APIError$1("BAD_REQUEST", {
1034
788
  error_description: "opaque access token not found",
1035
789
  error: "invalid_request"
1036
790
  });
@@ -1041,7 +795,7 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
1041
795
  value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
1042
796
  }]
1043
797
  });
1044
- if (!accessToken) throw new APIError("BAD_REQUEST", {
798
+ if (!accessToken) throw new APIError$1("BAD_REQUEST", {
1045
799
  error_description: "opaque access token not found",
1046
800
  error: "invalid_token"
1047
801
  });
@@ -1079,8 +833,8 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
1079
833
  client_id: accessToken.clientId,
1080
834
  sub: user?.id,
1081
835
  sid: sessionId,
1082
- exp: Math.floor(accessToken.expiresAt.getTime() / 1e3),
1083
- iat: Math.floor(accessToken.createdAt.getTime() / 1e3),
836
+ exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
837
+ iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
1084
838
  scope: accessToken.scopes?.join(" ")
1085
839
  };
1086
840
  }
@@ -1097,7 +851,7 @@ async function validateRefreshToken(ctx, opts, token, clientId) {
1097
851
  value: await getStoredToken(opts.storeTokens, token, "refresh_token")
1098
852
  }]
1099
853
  });
1100
- if (!refreshToken) throw new APIError("BAD_REQUEST", {
854
+ if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
1101
855
  error_description: "token not found",
1102
856
  error: "invalid_token"
1103
857
  });
@@ -1123,8 +877,8 @@ async function validateRefreshToken(ctx, opts, token, clientId) {
1123
877
  iss: ((opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options)?.jwt?.issuer ?? ctx.context.baseURL,
1124
878
  sub: user?.id,
1125
879
  sid: sessionId,
1126
- exp: Math.floor(refreshToken.expiresAt.getTime() / 1e3),
1127
- iat: Math.floor(refreshToken.createdAt.getTime() / 1e3),
880
+ exp: Math.floor(new Date(refreshToken.expiresAt).getTime() / 1e3),
881
+ iat: Math.floor(new Date(refreshToken.createdAt).getTime() / 1e3),
1128
882
  scope: refreshToken.scopes?.join(" ")
1129
883
  };
1130
884
  }
@@ -1140,16 +894,16 @@ async function validateAccessToken(ctx, opts, token, clientId) {
1140
894
  try {
1141
895
  return await validateJwtAccessToken(ctx, opts, token, clientId);
1142
896
  } catch (err) {
1143
- if (err instanceof APIError) {} else if (err instanceof Error) throw err;
897
+ if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
1144
898
  else throw new Error(err);
1145
899
  }
1146
900
  try {
1147
901
  return await validateOpaqueAccessToken(ctx, opts, token, clientId);
1148
902
  } catch (err) {
1149
- if (err instanceof APIError) {} else if (err instanceof Error) throw err;
903
+ if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
1150
904
  else throw new Error("Unknown error validating access token");
1151
905
  }
1152
- throw new APIError("BAD_REQUEST", {
906
+ throw new APIError$1("BAD_REQUEST", {
1153
907
  error_description: "Invalid access token",
1154
908
  error: "invalid_request"
1155
909
  });
@@ -1162,12 +916,12 @@ async function introspectEndpoint(ctx, opts) {
1162
916
  client_id = res?.client_id;
1163
917
  client_secret = res?.client_secret;
1164
918
  }
1165
- if (!client_id || !client_secret) throw new APIError("UNAUTHORIZED", {
919
+ if (!client_id || !client_secret) throw new APIError$1("UNAUTHORIZED", {
1166
920
  error_description: "missing required credentials",
1167
921
  error: "invalid_client"
1168
922
  });
1169
923
  if (token && typeof token === "string" && token.startsWith("Bearer ")) token = token.replace("Bearer ", "");
1170
- if (!token?.length) throw new APIError("BAD_REQUEST", {
924
+ if (!token?.length) throw new APIError$1("BAD_REQUEST", {
1171
925
  error_description: "missing a required token for introspection",
1172
926
  error: "invalid_request"
1173
927
  });
@@ -1176,7 +930,7 @@ async function introspectEndpoint(ctx, opts) {
1176
930
  if (token_type_hint === void 0 || token_type_hint === "access_token") try {
1177
931
  return await validateAccessToken(ctx, opts, token, client.clientId);
1178
932
  } catch (error) {
1179
- if (error instanceof APIError) {
933
+ if (error instanceof APIError$1) {
1180
934
  if (token_type_hint === "access_token") throw error;
1181
935
  } else if (error instanceof Error) throw error;
1182
936
  else throw new Error(error);
@@ -1184,25 +938,25 @@ async function introspectEndpoint(ctx, opts) {
1184
938
  if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
1185
939
  return await validateRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId);
1186
940
  } catch (error) {
1187
- if (error instanceof APIError) {
941
+ if (error instanceof APIError$1) {
1188
942
  if (token_type_hint === "refresh_token") throw error;
1189
943
  } else if (error instanceof Error) throw error;
1190
944
  else throw new Error(error);
1191
945
  }
1192
- throw new APIError("BAD_REQUEST", {
946
+ throw new APIError$1("BAD_REQUEST", {
1193
947
  error_description: "token not found",
1194
948
  error: "invalid_request"
1195
949
  });
1196
950
  } catch (error) {
1197
- if (error instanceof APIError) {
951
+ if (error instanceof APIError$1) {
1198
952
  if (error.name === "BAD_REQUEST") return { active: false };
1199
953
  throw error;
1200
954
  } else if (error instanceof Error) {
1201
955
  logger.error("Introspection error:", error.message, error.stack);
1202
- throw new APIError("INTERNAL_SERVER_ERROR");
956
+ throw new APIError$1("INTERNAL_SERVER_ERROR");
1203
957
  } else {
1204
958
  logger.error("Introspection error:", error);
1205
- throw new APIError("INTERNAL_SERVER_ERROR");
959
+ throw new APIError$1("INTERNAL_SERVER_ERROR");
1206
960
  }
1207
961
  }
1208
962
  }
@@ -1226,34 +980,34 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
1226
980
  try {
1227
981
  decoded = decodeJwt(id_token_hint);
1228
982
  } catch (_e) {
1229
- throw new APIError("UNAUTHORIZED", {
983
+ throw new APIError$1("UNAUTHORIZED", {
1230
984
  error_description: "invalid id token",
1231
985
  error: "invalid_token"
1232
986
  });
1233
987
  }
1234
988
  clientId = decoded?.aud;
1235
- if (!clientId) throw new APIError("INTERNAL_SERVER_ERROR", {
989
+ if (!clientId) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1236
990
  error_description: "id token missing audience",
1237
991
  error: "invalid_request"
1238
992
  });
1239
993
  }
1240
994
  const client = await getClient(ctx, opts, clientId);
1241
- if (!client) throw new APIError("BAD_REQUEST", {
995
+ if (!client) throw new APIError$1("BAD_REQUEST", {
1242
996
  error_description: "client doesn't exist",
1243
997
  error: "invalid_client"
1244
998
  });
1245
- if (client.disabled) throw new APIError("BAD_REQUEST", {
999
+ if (client.disabled) throw new APIError$1("BAD_REQUEST", {
1246
1000
  error_description: "client is disabled",
1247
1001
  error: "invalid_client"
1248
1002
  });
1249
- if (!client.enableEndSession) throw new APIError("UNAUTHORIZED", {
1003
+ if (!client.enableEndSession) throw new APIError$1("UNAUTHORIZED", {
1250
1004
  error_description: "client unable to logout",
1251
1005
  error: "invalid_client"
1252
1006
  });
1253
1007
  let idTokenPayload;
1254
1008
  if (opts.disableJwtPlugin) {
1255
1009
  const clientSecret = client.clientSecret;
1256
- if (!clientSecret) throw new APIError("UNAUTHORIZED", {
1010
+ if (!clientSecret) throw new APIError$1("UNAUTHORIZED", {
1257
1011
  error_description: "missing required credentials",
1258
1012
  error: "invalid_client"
1259
1013
  });
@@ -1266,25 +1020,25 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
1266
1020
  const idToken = new TextDecoder().decode(payload);
1267
1021
  idTokenPayload = JSON.parse(idToken);
1268
1022
  }
1269
- if (!idTokenPayload) throw new APIError("INTERNAL_SERVER_ERROR", {
1023
+ if (!idTokenPayload) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1270
1024
  error_description: "missing payload",
1271
1025
  error: "invalid_request"
1272
1026
  });
1273
- if ((jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL) !== idTokenPayload.iss) throw new APIError("INTERNAL_SERVER_ERROR", {
1027
+ if ((jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL) !== idTokenPayload.iss) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1274
1028
  error_description: "invalid issuer",
1275
1029
  error: "invalid_request"
1276
1030
  });
1277
1031
  const idTokenAudience = typeof idTokenPayload.aud === "string" ? [idTokenPayload.aud] : idTokenPayload.aud;
1278
- if (!idTokenAudience) throw new APIError("INTERNAL_SERVER_ERROR", {
1032
+ if (!idTokenAudience) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1279
1033
  error_description: "id token missing audience",
1280
1034
  error: "invalid_request"
1281
1035
  });
1282
- if (client_id && !idTokenAudience.includes(client_id)) throw new APIError("BAD_REQUEST", {
1036
+ if (client_id && !idTokenAudience.includes(client_id)) throw new APIError$1("BAD_REQUEST", {
1283
1037
  error_description: "audience mismatch",
1284
1038
  error: "invalid_request"
1285
1039
  });
1286
1040
  const sessionId = idTokenPayload.sid;
1287
- if (!sessionId) throw new APIError("INTERNAL_SERVER_ERROR", {
1041
+ if (!sessionId) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1288
1042
  error_description: "id token missing session",
1289
1043
  error: "invalid_request"
1290
1044
  });
@@ -1322,18 +1076,18 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
1322
1076
  //#endregion
1323
1077
  //#region src/register.ts
1324
1078
  async function registerEndpoint(ctx, opts) {
1325
- if (!opts.allowDynamicClientRegistration) throw new APIError$1("FORBIDDEN", {
1079
+ if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
1326
1080
  error: "access_denied",
1327
1081
  error_description: "Client registration is disabled"
1328
1082
  });
1329
1083
  const body = ctx.body;
1330
1084
  const session = await getSessionFromCtx(ctx);
1331
- if (!(session || opts.allowUnauthenticatedClientRegistration)) throw new APIError$1("UNAUTHORIZED", {
1085
+ if (!(session || opts.allowUnauthenticatedClientRegistration)) throw new APIError("UNAUTHORIZED", {
1332
1086
  error: "invalid_token",
1333
1087
  error_description: "Authentication required for client registration"
1334
1088
  });
1335
1089
  const isPublic = body.token_endpoint_auth_method === "none";
1336
- if (!session && !isPublic) throw new APIError$1("UNAUTHORIZED", {
1090
+ if (!session && !isPublic) throw new APIError("UNAUTHORIZED", {
1337
1091
  error: "invalid_request",
1338
1092
  error_description: "Authentication required for confidential client registration"
1339
1093
  });
@@ -1343,22 +1097,22 @@ async function registerEndpoint(ctx, opts) {
1343
1097
  async function checkOAuthClient(client, opts, settings) {
1344
1098
  const isPublic = client.token_endpoint_auth_method === "none";
1345
1099
  if (client.type) {
1346
- if (isPublic && !(client.type === "native" || client.type === "user-agent-based")) throw new APIError$1("BAD_REQUEST", {
1100
+ if (isPublic && !(client.type === "native" || client.type === "user-agent-based")) throw new APIError("BAD_REQUEST", {
1347
1101
  error: "invalid_client_metadata",
1348
1102
  error_description: `Type must be 'native' or 'user-agent-based' for public applications`
1349
1103
  });
1350
- else if (!isPublic && !(client.type === "web")) throw new APIError$1("BAD_REQUEST", {
1104
+ else if (!isPublic && !(client.type === "web")) throw new APIError("BAD_REQUEST", {
1351
1105
  error: "invalid_client_metadata",
1352
1106
  error_description: `Type must be 'web' for confidential applications`
1353
1107
  });
1354
1108
  }
1355
- if ((!client.grant_types || client.grant_types.includes("authorization_code")) && (!client.redirect_uris || client.redirect_uris.length === 0)) throw new APIError$1("BAD_REQUEST", {
1109
+ if ((!client.grant_types || client.grant_types.includes("authorization_code")) && (!client.redirect_uris || client.redirect_uris.length === 0)) throw new APIError("BAD_REQUEST", {
1356
1110
  error: "invalid_redirect_uri",
1357
1111
  error_description: "Redirect URIs are required for authorization_code and implicit grant types"
1358
1112
  });
1359
1113
  const grantTypes = client.grant_types ?? ["authorization_code"];
1360
1114
  const responseTypes = client.response_types ?? ["code"];
1361
- if (grantTypes.includes("authorization_code") && !responseTypes.includes("code")) throw new APIError$1("BAD_REQUEST", {
1115
+ if (grantTypes.includes("authorization_code") && !responseTypes.includes("code")) throw new APIError("BAD_REQUEST", {
1362
1116
  error: "invalid_client_metadata",
1363
1117
  error_description: "When 'authorization_code' grant type is used, 'code' response type must be included"
1364
1118
  });
@@ -1366,11 +1120,15 @@ async function checkOAuthClient(client, opts, settings) {
1366
1120
  const allowedScopes = settings?.isRegister ? opts.clientRegistrationAllowedScopes ?? opts.scopes : opts.scopes;
1367
1121
  if (allowedScopes) {
1368
1122
  const validScopes = new Set(allowedScopes);
1369
- for (const requestedScope of requestedScopes ?? []) if (!validScopes?.has(requestedScope)) throw new APIError$1("BAD_REQUEST", {
1123
+ for (const requestedScope of requestedScopes ?? []) if (!validScopes?.has(requestedScope)) throw new APIError("BAD_REQUEST", {
1370
1124
  error: "invalid_scope",
1371
1125
  error_description: `cannot request scope ${requestedScope}`
1372
1126
  });
1373
1127
  }
1128
+ if (settings?.isRegister && client.require_pkce === false) throw new APIError("BAD_REQUEST", {
1129
+ error: "invalid_client_metadata",
1130
+ error_description: `pkce is required for registered clients.`
1131
+ });
1374
1132
  }
1375
1133
  async function createOAuthClientEndpoint(ctx, opts, settings) {
1376
1134
  const body = ctx.body;
@@ -1385,7 +1143,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1385
1143
  user: session?.user,
1386
1144
  session: session?.session
1387
1145
  }) : void 0;
1388
- const schema$1 = oauthToSchema({
1146
+ const schema = oauthToSchema({
1389
1147
  ...body ?? {},
1390
1148
  disabled: void 0,
1391
1149
  jwks: void 0,
@@ -1400,7 +1158,11 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1400
1158
  });
1401
1159
  const client = await ctx.context.adapter.create({
1402
1160
  model: "oauthClient",
1403
- data: schema$1
1161
+ data: {
1162
+ ...schema,
1163
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3),
1164
+ updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
1165
+ }
1404
1166
  });
1405
1167
  return ctx.json(schemaToOAuth({
1406
1168
  ...client,
@@ -1420,7 +1182,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1420
1182
  * @returns
1421
1183
  */
1422
1184
  function oauthToSchema(input) {
1423
- 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: _jwks, 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, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1185
+ 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: _jwks, 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, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1424
1186
  const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
1425
1187
  const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
1426
1188
  const scopes = _scope?.split(" ");
@@ -1454,6 +1216,7 @@ function oauthToSchema(input) {
1454
1216
  type,
1455
1217
  skipConsent,
1456
1218
  enableEndSession,
1219
+ requirePKCE,
1457
1220
  referenceId,
1458
1221
  metadata: Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0
1459
1222
  };
@@ -1465,9 +1228,9 @@ function oauthToSchema(input) {
1465
1228
  * @returns
1466
1229
  */
1467
1230
  function schemaToOAuth(input) {
1468
- 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, skipConsent, enableEndSession, referenceId, metadata } = input;
1469
- const _expiresAt = expiresAt ? Math.round(expiresAt.getTime() / 1e3) : void 0;
1470
- const _createdAt = createdAt ? Math.round(createdAt.getTime() / 1e3) : void 0;
1231
+ 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, skipConsent, enableEndSession, requirePKCE, referenceId, metadata } = input;
1232
+ const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
1233
+ const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
1471
1234
  const _scopes = scopes?.join(" ");
1472
1235
  return {
1473
1236
  ...parseClientMetadata(metadata),
@@ -1496,14 +1259,26 @@ function schemaToOAuth(input) {
1496
1259
  disabled: disabled ?? void 0,
1497
1260
  skip_consent: skipConsent ?? void 0,
1498
1261
  enable_end_session: enableEndSession ?? void 0,
1262
+ require_pkce: requirePKCE ?? void 0,
1499
1263
  reference_id: referenceId ?? void 0
1500
1264
  };
1501
1265
  }
1502
1266
 
1503
1267
  //#endregion
1504
1268
  //#region src/types/zod.ts
1269
+ const DANGEROUS_SCHEMES = [
1270
+ "javascript:",
1271
+ "data:",
1272
+ "vbscript:"
1273
+ ];
1274
+ function isLocalhost(hostname) {
1275
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
1276
+ }
1505
1277
  /**
1506
- * Reusable URL validation that disallows javascript: scheme
1278
+ * Reusable URL validation for OAuth redirect URIs.
1279
+ * - Blocks dangerous schemes (javascript:, data:, vbscript:)
1280
+ * - For http/https: requires HTTPS (HTTP allowed only for localhost)
1281
+ * - Allows custom schemes for mobile apps (e.g., myapp://callback)
1507
1282
  */
1508
1283
  const SafeUrlSchema = z.url().superRefine((val, ctx) => {
1509
1284
  if (!URL.canParse(val)) {
@@ -1514,33 +1289,44 @@ const SafeUrlSchema = z.url().superRefine((val, ctx) => {
1514
1289
  });
1515
1290
  return z.NEVER;
1516
1291
  }
1517
- }).refine((url) => {
1518
- const u = new URL(url);
1519
- return u.protocol !== "javascript:" && u.protocol !== "data:" && u.protocol !== "vbscript:";
1520
- }, { message: "URL cannot use javascript:, data:, or vbscript: scheme" });
1292
+ const u = new URL(val);
1293
+ if (DANGEROUS_SCHEMES.includes(u.protocol)) {
1294
+ ctx.addIssue({
1295
+ code: "custom",
1296
+ message: "URL cannot use javascript:, data:, or vbscript: scheme"
1297
+ });
1298
+ return;
1299
+ }
1300
+ if (u.protocol === "http:" || u.protocol === "https:") {
1301
+ if (u.protocol === "http:" && !isLocalhost(u.hostname)) ctx.addIssue({
1302
+ code: "custom",
1303
+ message: "Redirect URI must use HTTPS (HTTP allowed only for localhost)"
1304
+ });
1305
+ }
1306
+ });
1521
1307
 
1522
1308
  //#endregion
1523
1309
  //#region src/oauthClient/endpoints.ts
1524
1310
  async function getClientEndpoint(ctx, opts) {
1525
1311
  const session = await getSessionFromCtx(ctx);
1526
- if (!session) throw new APIError$1("UNAUTHORIZED");
1527
- if (!ctx.headers) throw new APIError$1("BAD_REQUEST");
1312
+ if (!session) throw new APIError("UNAUTHORIZED");
1313
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1528
1314
  if (opts.clientPrivileges && !await opts.clientPrivileges({
1529
1315
  headers: ctx.headers,
1530
1316
  action: "read",
1531
1317
  session: session.session,
1532
1318
  user: session.user
1533
- })) throw new APIError$1("UNAUTHORIZED");
1319
+ })) throw new APIError("UNAUTHORIZED");
1534
1320
  const client = await getClient(ctx, opts, ctx.query.client_id);
1535
- if (!client) throw new APIError$1("NOT_FOUND", {
1321
+ if (!client) throw new APIError("NOT_FOUND", {
1536
1322
  error_description: "client not found",
1537
1323
  error: "not_found"
1538
1324
  });
1539
1325
  if (client.userId) {
1540
- if (client.userId !== session.user.id) throw new APIError$1("UNAUTHORIZED");
1326
+ if (client.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
1541
1327
  } else if (client.referenceId && opts.clientReference) {
1542
- if (client.referenceId !== await opts.clientReference(session)) throw new APIError$1("UNAUTHORIZED");
1543
- } else throw new APIError$1("UNAUTHORIZED");
1328
+ if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
1329
+ } else throw new APIError("UNAUTHORIZED");
1544
1330
  const res = schemaToOAuth(client);
1545
1331
  res.client_secret = void 0;
1546
1332
  return res;
@@ -1551,11 +1337,11 @@ async function getClientEndpoint(ctx, opts) {
1551
1337
  */
1552
1338
  async function getClientPublicEndpoint(ctx, opts) {
1553
1339
  const client = await getClient(ctx, opts, ctx.query.client_id);
1554
- if (!client) throw new APIError$1("NOT_FOUND", {
1340
+ if (!client) throw new APIError("NOT_FOUND", {
1555
1341
  error_description: "client not found",
1556
1342
  error: "not_found"
1557
1343
  });
1558
- if (client.disabled) throw new APIError$1("NOT_FOUND", {
1344
+ if (client.disabled) throw new APIError("NOT_FOUND", {
1559
1345
  error_description: "client not found",
1560
1346
  error: "not_found"
1561
1347
  });
@@ -1571,14 +1357,14 @@ async function getClientPublicEndpoint(ctx, opts) {
1571
1357
  }
1572
1358
  async function getClientsEndpoint(ctx, opts) {
1573
1359
  const session = await getSessionFromCtx(ctx);
1574
- if (!session) throw new APIError$1("UNAUTHORIZED");
1575
- if (!ctx.headers) throw new APIError$1("BAD_REQUEST");
1360
+ if (!session) throw new APIError("UNAUTHORIZED");
1361
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1576
1362
  if (opts.clientPrivileges && !await opts.clientPrivileges({
1577
1363
  headers: ctx.headers,
1578
1364
  action: "list",
1579
1365
  session: session.session,
1580
1366
  user: session.user
1581
- })) throw new APIError$1("UNAUTHORIZED");
1367
+ })) throw new APIError("UNAUTHORIZED");
1582
1368
  const referenceId = await opts.clientReference?.(session);
1583
1369
  if (referenceId) return await ctx.context.adapter.findMany({
1584
1370
  model: "oauthClient",
@@ -1589,9 +1375,9 @@ async function getClientsEndpoint(ctx, opts) {
1589
1375
  }).then((res) => {
1590
1376
  if (!res) return null;
1591
1377
  return res.map((v) => {
1592
- const res$1 = schemaToOAuth(v);
1593
- res$1.client_secret = void 0;
1594
- return res$1;
1378
+ const res = schemaToOAuth(v);
1379
+ res.client_secret = void 0;
1380
+ return res;
1595
1381
  });
1596
1382
  });
1597
1383
  else if (session.user.id) return await ctx.context.adapter.findMany({
@@ -1603,38 +1389,38 @@ async function getClientsEndpoint(ctx, opts) {
1603
1389
  }).then((res) => {
1604
1390
  if (!res) return null;
1605
1391
  return res.map((v) => {
1606
- const res$1 = schemaToOAuth(v);
1607
- res$1.client_secret = void 0;
1608
- return res$1;
1392
+ const res = schemaToOAuth(v);
1393
+ res.client_secret = void 0;
1394
+ return res;
1609
1395
  });
1610
1396
  });
1611
- else throw new APIError$1("BAD_REQUEST", { message: "either user_id or reference_id must be provided" });
1397
+ else throw new APIError("BAD_REQUEST", { message: "either user_id or reference_id must be provided" });
1612
1398
  }
1613
1399
  async function deleteClientEndpoint(ctx, opts) {
1614
1400
  const session = await getSessionFromCtx(ctx);
1615
- if (!session) throw new APIError$1("UNAUTHORIZED");
1616
- if (!ctx.headers) throw new APIError$1("BAD_REQUEST");
1401
+ if (!session) throw new APIError("UNAUTHORIZED");
1402
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1617
1403
  if (opts.clientPrivileges && !await opts.clientPrivileges({
1618
1404
  headers: ctx.headers,
1619
1405
  action: "delete",
1620
1406
  session: session.session,
1621
1407
  user: session.user
1622
- })) throw new APIError$1("UNAUTHORIZED");
1408
+ })) throw new APIError("UNAUTHORIZED");
1623
1409
  const clientId = ctx.body.client_id;
1624
- if (opts.cachedTrustedClients?.has(clientId)) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1410
+ if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
1625
1411
  error_description: "trusted clients must be updated manually",
1626
1412
  error: "invalid_client"
1627
1413
  });
1628
1414
  const client = await getClient(ctx, opts, clientId);
1629
- if (!client) throw new APIError$1("NOT_FOUND", {
1415
+ if (!client) throw new APIError("NOT_FOUND", {
1630
1416
  error_description: "client not found",
1631
1417
  error: "not_found"
1632
1418
  });
1633
1419
  if (client.userId) {
1634
- if (client.userId !== session.user.id) throw new APIError$1("UNAUTHORIZED");
1420
+ if (client.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
1635
1421
  } else if (client.referenceId && opts.clientReference) {
1636
- if (client.referenceId !== await opts.clientReference(session)) throw new APIError$1("UNAUTHORIZED");
1637
- } else throw new APIError$1("UNAUTHORIZED");
1422
+ if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
1423
+ } else throw new APIError("UNAUTHORIZED");
1638
1424
  await ctx.context.adapter.delete({
1639
1425
  model: "oauthClient",
1640
1426
  where: [{
@@ -1645,34 +1431,34 @@ async function deleteClientEndpoint(ctx, opts) {
1645
1431
  }
1646
1432
  async function updateClientEndpoint(ctx, opts) {
1647
1433
  const session = await getSessionFromCtx(ctx);
1648
- if (!session) throw new APIError$1("UNAUTHORIZED");
1649
- if (!ctx.headers) throw new APIError$1("BAD_REQUEST");
1434
+ if (!session) throw new APIError("UNAUTHORIZED");
1435
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1650
1436
  if (opts.clientPrivileges && !await opts.clientPrivileges({
1651
1437
  headers: ctx.headers,
1652
1438
  action: "update",
1653
1439
  session: session.session,
1654
1440
  user: session.user
1655
- })) throw new APIError$1("UNAUTHORIZED");
1441
+ })) throw new APIError("UNAUTHORIZED");
1656
1442
  const clientId = ctx.body.client_id;
1657
- if (opts.cachedTrustedClients?.has(clientId)) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1443
+ if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
1658
1444
  error_description: "trusted clients must be updated manually",
1659
1445
  error: "invalid_client"
1660
1446
  });
1661
1447
  const client = await getClient(ctx, opts, clientId);
1662
- if (!client) throw new APIError$1("NOT_FOUND", {
1448
+ if (!client) throw new APIError("NOT_FOUND", {
1663
1449
  error_description: "client not found",
1664
1450
  error: "not_found"
1665
1451
  });
1666
1452
  if (client.userId) {
1667
- if (client.userId !== session.user.id) throw new APIError$1("UNAUTHORIZED");
1453
+ if (client.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
1668
1454
  } else if (client.referenceId && opts.clientReference) {
1669
- if (client.referenceId !== await opts.clientReference(session)) throw new APIError$1("UNAUTHORIZED");
1670
- } else throw new APIError$1("UNAUTHORIZED");
1455
+ if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
1456
+ } else throw new APIError("UNAUTHORIZED");
1671
1457
  const updates = ctx.body.update;
1672
1458
  if (Object.keys(updates).length === 0) {
1673
- const res$1 = schemaToOAuth(client);
1674
- res$1.client_secret = void 0;
1675
- return res$1;
1459
+ const res = schemaToOAuth(client);
1460
+ res.client_secret = void 0;
1461
+ return res;
1676
1462
  }
1677
1463
  await checkOAuthClient({
1678
1464
  ...schemaToOAuth(client),
@@ -1684,9 +1470,12 @@ async function updateClientEndpoint(ctx, opts) {
1684
1470
  field: "clientId",
1685
1471
  value: clientId
1686
1472
  }],
1687
- update: oauthToSchema(updates)
1473
+ update: {
1474
+ ...oauthToSchema(updates),
1475
+ updatedAt: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3)
1476
+ }
1688
1477
  });
1689
- if (!updatedClient) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1478
+ if (!updatedClient) throw new APIError("INTERNAL_SERVER_ERROR", {
1690
1479
  error_description: "unable to update client",
1691
1480
  error: "invalid_client"
1692
1481
  });
@@ -1696,30 +1485,30 @@ async function updateClientEndpoint(ctx, opts) {
1696
1485
  }
1697
1486
  async function rotateClientSecretEndpoint(ctx, opts) {
1698
1487
  const session = await getSessionFromCtx(ctx);
1699
- if (!session) throw new APIError$1("UNAUTHORIZED");
1700
- if (!ctx.headers) throw new APIError$1("BAD_REQUEST");
1488
+ if (!session) throw new APIError("UNAUTHORIZED");
1489
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1701
1490
  if (opts.clientPrivileges && !await opts.clientPrivileges({
1702
1491
  headers: ctx.headers,
1703
1492
  action: "rotate",
1704
1493
  session: session.session,
1705
1494
  user: session.user
1706
- })) throw new APIError$1("UNAUTHORIZED");
1495
+ })) throw new APIError("UNAUTHORIZED");
1707
1496
  const clientId = ctx.body.client_id;
1708
- if (opts.cachedTrustedClients?.has(clientId)) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1497
+ if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
1709
1498
  error_description: "trusted clients must be updated manually",
1710
1499
  error: "invalid_client"
1711
1500
  });
1712
1501
  const client = await getClient(ctx, opts, clientId);
1713
- if (!client) throw new APIError$1("NOT_FOUND", {
1502
+ if (!client) throw new APIError("NOT_FOUND", {
1714
1503
  error_description: "client not found",
1715
1504
  error: "not_found"
1716
1505
  });
1717
1506
  if (client.userId) {
1718
- if (client.userId !== session.user.id) throw new APIError$1("UNAUTHORIZED");
1507
+ if (client.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
1719
1508
  } else if (client.referenceId && opts.clientReference) {
1720
- if (client.referenceId !== await opts.clientReference(session)) throw new APIError$1("UNAUTHORIZED");
1721
- } else throw new APIError$1("UNAUTHORIZED");
1722
- if (client.public || !client.clientSecret) throw new APIError$1("BAD_REQUEST", {
1509
+ if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
1510
+ } else throw new APIError("UNAUTHORIZED");
1511
+ if (client.public || !client.clientSecret) throw new APIError("BAD_REQUEST", {
1723
1512
  error_description: "public clients cannot be updated",
1724
1513
  error: "invalid_client"
1725
1514
  });
@@ -1732,11 +1521,11 @@ async function rotateClientSecretEndpoint(ctx, opts) {
1732
1521
  value: clientId
1733
1522
  }],
1734
1523
  update: {
1735
- ...schemaToOAuth(client),
1736
- clientSecret: storedClientSecret
1524
+ clientSecret: storedClientSecret,
1525
+ updatedAt: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3)
1737
1526
  }
1738
1527
  });
1739
- if (!updatedClient) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1528
+ if (!updatedClient) throw new APIError("INTERNAL_SERVER_ERROR", {
1740
1529
  error_description: "unable to update client",
1741
1530
  error: "invalid_client"
1742
1531
  });
@@ -1782,6 +1571,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
1782
1571
  client_secret_expires_at: z.union([z.string(), z.number()]).optional().default(0),
1783
1572
  skip_consent: z.boolean().optional(),
1784
1573
  enable_end_session: z.boolean().optional(),
1574
+ require_pkce: z.boolean().optional(),
1785
1575
  metadata: z.record(z.string(), z.unknown()).optional()
1786
1576
  }),
1787
1577
  metadata: {
@@ -1908,6 +1698,11 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
1908
1698
  type: "boolean",
1909
1699
  description: "Whether the client is disabled"
1910
1700
  },
1701
+ require_pkce: {
1702
+ type: "boolean",
1703
+ description: "Whether the client requires PKCE",
1704
+ default: true
1705
+ },
1911
1706
  metadata: {
1912
1707
  type: "object",
1913
1708
  additionalProperties: true,
@@ -2221,23 +2016,23 @@ async function getConsent(ctx, opts, id) {
2221
2016
  }
2222
2017
  async function getConsentEndpoint(ctx, opts) {
2223
2018
  const session = await getSessionFromCtx(ctx);
2224
- if (!session) throw new APIError$1("UNAUTHORIZED");
2019
+ if (!session) throw new APIError("UNAUTHORIZED");
2225
2020
  const { id } = ctx.query;
2226
- if (!id) throw new APIError$1("NOT_FOUND", {
2021
+ if (!id) throw new APIError("NOT_FOUND", {
2227
2022
  error_description: "missing id parameter",
2228
2023
  error: "not_found"
2229
2024
  });
2230
2025
  const consent = await getConsent(ctx, opts, id);
2231
- if (!consent) throw new APIError$1("NOT_FOUND", {
2026
+ if (!consent) throw new APIError("NOT_FOUND", {
2232
2027
  error_description: "no consent",
2233
2028
  error: "not_found"
2234
2029
  });
2235
- if (consent.userId !== session.user.id) throw new APIError$1("UNAUTHORIZED");
2030
+ if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
2236
2031
  return consent;
2237
2032
  }
2238
2033
  async function getConsentsEndpoint(ctx, opts) {
2239
2034
  const session = await getSessionFromCtx(ctx);
2240
- if (!session) throw new APIError$1("UNAUTHORIZED");
2035
+ if (!session) throw new APIError("UNAUTHORIZED");
2241
2036
  return await ctx.context.adapter.findMany({
2242
2037
  model: "oauthConsent",
2243
2038
  where: [{
@@ -2248,18 +2043,18 @@ async function getConsentsEndpoint(ctx, opts) {
2248
2043
  }
2249
2044
  async function deleteConsentEndpoint(ctx, opts) {
2250
2045
  const session = await getSessionFromCtx(ctx);
2251
- if (!session) throw new APIError$1("UNAUTHORIZED");
2046
+ if (!session) throw new APIError("UNAUTHORIZED");
2252
2047
  const { id } = ctx.body;
2253
- if (!id) throw new APIError$1("NOT_FOUND", {
2048
+ if (!id) throw new APIError("NOT_FOUND", {
2254
2049
  error_description: "missing id parameter",
2255
2050
  error: "not_found"
2256
2051
  });
2257
2052
  const consent = await getConsent(ctx, opts, id);
2258
- if (!consent) throw new APIError$1("NOT_FOUND", {
2053
+ if (!consent) throw new APIError("NOT_FOUND", {
2259
2054
  error_description: "no consent",
2260
2055
  error: "not_found"
2261
2056
  });
2262
- if (consent.userId !== session.user.id) throw new APIError$1("UNAUTHORIZED");
2057
+ if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
2263
2058
  await ctx.context.adapter.delete({
2264
2059
  model: "oauthConsent",
2265
2060
  where: [{
@@ -2270,27 +2065,27 @@ async function deleteConsentEndpoint(ctx, opts) {
2270
2065
  }
2271
2066
  async function updateConsentEndpoint(ctx, opts) {
2272
2067
  const session = await getSessionFromCtx(ctx);
2273
- if (!session) throw new APIError$1("UNAUTHORIZED");
2068
+ if (!session) throw new APIError("UNAUTHORIZED");
2274
2069
  const { id } = ctx.body;
2275
- if (!id) throw new APIError$1("NOT_FOUND", {
2070
+ if (!id) throw new APIError("NOT_FOUND", {
2276
2071
  error_description: "missing id parameter",
2277
2072
  error: "not_found"
2278
2073
  });
2279
2074
  const consent = await getConsent(ctx, opts, id);
2280
- if (!consent) throw new APIError$1("NOT_FOUND", {
2075
+ if (!consent) throw new APIError("NOT_FOUND", {
2281
2076
  error_description: "no consent",
2282
2077
  error: "not_found"
2283
2078
  });
2284
2079
  const client = await getClient(ctx, opts, consent.clientId);
2285
- if (!consent) throw new APIError$1("NOT_FOUND", {
2080
+ if (!consent) throw new APIError("NOT_FOUND", {
2286
2081
  error_description: "no consent",
2287
2082
  error: "not_found"
2288
2083
  });
2289
- if (consent.userId !== session.user.id) throw new APIError$1("UNAUTHORIZED");
2084
+ if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
2290
2085
  const allowedScopes = client?.scopes ?? opts.scopes ?? [];
2291
2086
  const updates = ctx.body.update;
2292
2087
  const scopes = updates.scopes;
2293
- if (scopes && !scopes.every((val) => allowedScopes?.includes(val))) throw new APIError$1("BAD_REQUEST", {
2088
+ if (scopes && !scopes.every((val) => allowedScopes?.includes(val))) throw new APIError("BAD_REQUEST", {
2294
2089
  error_description: `unable to provide scopes to ${client?.referenceId ?? client?.userId}`,
2295
2090
  error: "invalid_request"
2296
2091
  });
@@ -2373,7 +2168,7 @@ async function revokeJwtAccessToken(ctx, opts, token) {
2373
2168
  });
2374
2169
  } catch (error) {
2375
2170
  if (error instanceof Error) {
2376
- if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError("BAD_REQUEST", {
2171
+ if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
2377
2172
  error_description: "invalid JWT signature",
2378
2173
  error: "invalid_request"
2379
2174
  });
@@ -2390,7 +2185,7 @@ async function revokeJwtAccessToken(ctx, opts, token) {
2390
2185
  async function revokeOpaqueAccessToken(ctx, opts, token, clientId) {
2391
2186
  let tokenValue = token;
2392
2187
  if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
2393
- else throw new APIError("BAD_REQUEST", {
2188
+ else throw new APIError$1("BAD_REQUEST", {
2394
2189
  error_description: "opaque access token not found",
2395
2190
  error: "invalid_request"
2396
2191
  });
@@ -2401,7 +2196,7 @@ async function revokeOpaqueAccessToken(ctx, opts, token, clientId) {
2401
2196
  value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
2402
2197
  }]
2403
2198
  });
2404
- if (!accessToken) throw new APIError("BAD_REQUEST", {
2199
+ if (!accessToken) throw new APIError$1("BAD_REQUEST", {
2405
2200
  error_description: "opaque access token not found",
2406
2201
  error: "invalid_request"
2407
2202
  });
@@ -2431,7 +2226,7 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
2431
2226
  value: await getStoredToken(opts.storeTokens, token, "refresh_token")
2432
2227
  }]
2433
2228
  });
2434
- if (!refreshToken) throw new APIError("BAD_REQUEST", {
2229
+ if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
2435
2230
  error_description: "token not found",
2436
2231
  error: "invalid_request"
2437
2232
  });
@@ -2446,7 +2241,7 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
2446
2241
  value: refreshToken.userId
2447
2242
  }]
2448
2243
  });
2449
- throw new APIError("BAD_REQUEST", {
2244
+ throw new APIError$1("BAD_REQUEST", {
2450
2245
  error_description: "refresh token revoked",
2451
2246
  error: "invalid_request"
2452
2247
  });
@@ -2476,16 +2271,16 @@ async function revokeAccessToken(ctx, opts, clientId, token) {
2476
2271
  try {
2477
2272
  return await revokeJwtAccessToken(ctx, opts, token);
2478
2273
  } catch (err) {
2479
- if (err instanceof APIError) {} else if (err instanceof Error) throw err;
2274
+ if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
2480
2275
  else throw new Error(err);
2481
2276
  }
2482
2277
  try {
2483
2278
  return await revokeOpaqueAccessToken(ctx, opts, token, clientId);
2484
2279
  } catch (err) {
2485
- if (err instanceof APIError) {} else if (err instanceof Error) throw err;
2280
+ if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
2486
2281
  else throw new Error("Unknown error validating access token");
2487
2282
  }
2488
- throw new APIError("BAD_REQUEST", {
2283
+ throw new APIError$1("BAD_REQUEST", {
2489
2284
  error_description: "Invalid access token",
2490
2285
  error: "invalid_request"
2491
2286
  });
@@ -2498,12 +2293,12 @@ async function revokeEndpoint(ctx, opts) {
2498
2293
  client_id = res?.client_id;
2499
2294
  client_secret = res?.client_secret;
2500
2295
  }
2501
- if (!client_id) throw new APIError("UNAUTHORIZED", {
2296
+ if (!client_id) throw new APIError$1("UNAUTHORIZED", {
2502
2297
  error_description: "missing required credentials",
2503
2298
  error: "invalid_client"
2504
2299
  });
2505
2300
  if (typeof token === "string" && token.startsWith("Bearer ")) token = token.replace("Bearer ", "");
2506
- if (!token?.length) throw new APIError("BAD_REQUEST", {
2301
+ if (!token?.length) throw new APIError$1("BAD_REQUEST", {
2507
2302
  error_description: "missing a required token for introspection",
2508
2303
  error: "invalid_request"
2509
2304
  });
@@ -2512,7 +2307,7 @@ async function revokeEndpoint(ctx, opts) {
2512
2307
  if (token_type_hint === void 0 || token_type_hint === "access_token") try {
2513
2308
  return await revokeAccessToken(ctx, opts, client.clientId, token);
2514
2309
  } catch (error) {
2515
- if (error instanceof APIError) {
2310
+ if (error instanceof APIError$1) {
2516
2311
  if (token_type_hint === "access_token") throw error;
2517
2312
  } else if (error instanceof Error) throw error;
2518
2313
  else throw new Error(error);
@@ -2520,25 +2315,25 @@ async function revokeEndpoint(ctx, opts) {
2520
2315
  if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
2521
2316
  return await revokeRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId);
2522
2317
  } catch (error) {
2523
- if (error instanceof APIError) {
2318
+ if (error instanceof APIError$1) {
2524
2319
  if (token_type_hint === "refresh_token") throw error;
2525
2320
  } else if (error instanceof Error) throw error;
2526
2321
  else throw new Error(error);
2527
2322
  }
2528
- throw new APIError("BAD_REQUEST", {
2323
+ throw new APIError$1("BAD_REQUEST", {
2529
2324
  error_description: "token not found",
2530
2325
  error: "invalid_request"
2531
2326
  });
2532
2327
  } catch (error) {
2533
- if (error instanceof APIError) {
2328
+ if (error instanceof APIError$1) {
2534
2329
  if (error.name === "BAD_REQUEST") return null;
2535
2330
  throw error;
2536
2331
  } else if (error instanceof Error) {
2537
2332
  logger.error("Introspection error:", error.message, error.stack);
2538
- throw new APIError("INTERNAL_SERVER_ERROR");
2333
+ throw new APIError$1("INTERNAL_SERVER_ERROR");
2539
2334
  } else {
2540
2335
  logger.error("Introspection error:", error);
2541
- throw new APIError("INTERNAL_SERVER_ERROR");
2336
+ throw new APIError$1("INTERNAL_SERVER_ERROR");
2542
2337
  }
2543
2338
  }
2544
2339
  }
@@ -2655,6 +2450,10 @@ const schema = {
2655
2450
  type: "string",
2656
2451
  required: false
2657
2452
  },
2453
+ requirePKCE: {
2454
+ type: "boolean",
2455
+ required: false
2456
+ },
2658
2457
  referenceId: {
2659
2458
  type: "string",
2660
2459
  required: false
@@ -2705,6 +2504,10 @@ const schema = {
2705
2504
  type: "date",
2706
2505
  required: false
2707
2506
  },
2507
+ authTime: {
2508
+ type: "date",
2509
+ required: false
2510
+ },
2708
2511
  scopes: {
2709
2512
  type: "string[]",
2710
2513
  required: true
@@ -2798,6 +2601,7 @@ const schema = {
2798
2601
  //#endregion
2799
2602
  //#region src/oauth.ts
2800
2603
  const oAuthState = defineRequestState(() => null);
2604
+ const getOAuthProviderState = oAuthState.get;
2801
2605
  /**
2802
2606
  * oAuth 2.1 provider plugin for Better Auth.
2803
2607
  *
@@ -2886,7 +2690,7 @@ const oauthProvider = (options) => {
2886
2690
  queryParams.delete("sig");
2887
2691
  queryParams = new URLSearchParams(queryParams);
2888
2692
  const verifySig = await makeSignature(queryParams.toString(), ctx.context.secret);
2889
- if (!sig || !constantTimeEqual(sig, verifySig) || /* @__PURE__ */ new Date(exp * 1e3) < /* @__PURE__ */ new Date()) throw new APIError$1("BAD_REQUEST", { error: "invalid_signature" });
2693
+ if (!sig || !constantTimeEqual(sig, verifySig) || /* @__PURE__ */ new Date(exp * 1e3) < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", { error: "invalid_signature" });
2890
2694
  queryParams.delete("exp");
2891
2695
  await oAuthState.set({ query: new URLSearchParams(queryParams).toString() });
2892
2696
  if (ctx.path === "/sign-in/social" || ctx.path === "/sign-in/oauth2") {
@@ -2920,13 +2724,18 @@ const oauthProvider = (options) => {
2920
2724
  metadata: { SERVER_ONLY: true }
2921
2725
  }, async (ctx) => {
2922
2726
  if (opts.scopes && opts.scopes.includes("openid")) return oidcServerMetadata(ctx, opts);
2923
- else return authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, { scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes });
2727
+ else return authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
2728
+ scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
2729
+ public_client_supported: opts.allowUnauthenticatedClientRegistration,
2730
+ grant_types_supported: opts.grantTypes,
2731
+ jwt_disabled: opts.disableJwtPlugin
2732
+ });
2924
2733
  }),
2925
2734
  getOpenIdConfig: createAuthEndpoint("/.well-known/openid-configuration", {
2926
2735
  method: "GET",
2927
2736
  metadata: { SERVER_ONLY: true }
2928
2737
  }, async (ctx) => {
2929
- if (opts.scopes && !opts.scopes.includes("openid")) throw new APIError$1("NOT_FOUND");
2738
+ if (opts.scopes && !opts.scopes.includes("openid")) throw new APIError("NOT_FOUND");
2930
2739
  return oidcServerMetadata(ctx, opts);
2931
2740
  }),
2932
2741
  oauth2Authorize: createAuthEndpoint("/oauth2/authorize", {
@@ -2941,6 +2750,7 @@ const oauthProvider = (options) => {
2941
2750
  code_challenge_method: z.enum(["S256"]).optional(),
2942
2751
  nonce: z.string().optional(),
2943
2752
  prompt: z.enum([
2753
+ "none",
2944
2754
  "consent",
2945
2755
  "login",
2946
2756
  "create",
@@ -3715,9 +3525,354 @@ const oauthProvider = (options) => {
3715
3525
  updateOAuthConsent: updateOAuthConsent(opts),
3716
3526
  deleteOAuthConsent: deleteOAuthConsent(opts)
3717
3527
  },
3718
- schema: mergeSchema(schema, opts?.schema)
3528
+ schema: mergeSchema(schema, opts?.schema),
3529
+ rateLimit: [
3530
+ ...opts.rateLimit?.token !== false ? [{
3531
+ pathMatcher: (path) => path === "/oauth2/token",
3532
+ window: opts.rateLimit?.token?.window ?? 60,
3533
+ max: opts.rateLimit?.token?.max ?? 20
3534
+ }] : [],
3535
+ ...opts.rateLimit?.authorize !== false ? [{
3536
+ pathMatcher: (path) => path === "/oauth2/authorize",
3537
+ window: opts.rateLimit?.authorize?.window ?? 60,
3538
+ max: opts.rateLimit?.authorize?.max ?? 30
3539
+ }] : [],
3540
+ ...opts.rateLimit?.introspect !== false ? [{
3541
+ pathMatcher: (path) => path === "/oauth2/introspect",
3542
+ window: opts.rateLimit?.introspect?.window ?? 60,
3543
+ max: opts.rateLimit?.introspect?.max ?? 100
3544
+ }] : [],
3545
+ ...opts.rateLimit?.revoke !== false ? [{
3546
+ pathMatcher: (path) => path === "/oauth2/revoke",
3547
+ window: opts.rateLimit?.revoke?.window ?? 60,
3548
+ max: opts.rateLimit?.revoke?.max ?? 30
3549
+ }] : [],
3550
+ ...opts.rateLimit?.register !== false ? [{
3551
+ pathMatcher: (path) => path === "/oauth2/register",
3552
+ window: opts.rateLimit?.register?.window ?? 60,
3553
+ max: opts.rateLimit?.register?.max ?? 5
3554
+ }] : [],
3555
+ ...opts.rateLimit?.userinfo !== false ? [{
3556
+ pathMatcher: (path) => path === "/oauth2/userinfo",
3557
+ window: opts.rateLimit?.userinfo?.window ?? 60,
3558
+ max: opts.rateLimit?.userinfo?.max ?? 60
3559
+ }] : []
3560
+ ]
3561
+ };
3562
+ };
3563
+
3564
+ //#endregion
3565
+ //#region src/authorize.ts
3566
+ /**
3567
+ * Formats an error url
3568
+ */
3569
+ function formatErrorURL(url, error, description, state, iss) {
3570
+ const searchParams = new URLSearchParams({
3571
+ error,
3572
+ error_description: description
3573
+ });
3574
+ state && searchParams.append("state", state);
3575
+ iss && searchParams.append("iss", iss);
3576
+ return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
3577
+ }
3578
+ const handleRedirect = (ctx, uri) => {
3579
+ if (ctx.headers?.get("accept")?.includes("application/json")) return {
3580
+ redirect: true,
3581
+ url: uri.toString()
3582
+ };
3583
+ else throw ctx.redirect(uri);
3584
+ };
3585
+ /**
3586
+ * Validates that the issuer URL
3587
+ * - MUST use HTTPS scheme (HTTP allowed for localhost in dev)
3588
+ * - MUST NOT contain query components
3589
+ * - MUST NOT contain fragment components
3590
+ *
3591
+ * @returns The validated issuer URL, or a sanitized version if invalid
3592
+ */
3593
+ function validateIssuerUrl(issuer) {
3594
+ try {
3595
+ const url = new URL(issuer);
3596
+ const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
3597
+ if (url.protocol !== "https:" && !isLocalhost) url.protocol = "https:";
3598
+ url.search = "";
3599
+ url.hash = "";
3600
+ return url.toString().replace(/\/$/, "");
3601
+ } catch {
3602
+ return issuer;
3603
+ }
3604
+ }
3605
+ /**
3606
+ * Gets the issuer identifier
3607
+ */
3608
+ function getIssuer(ctx, opts) {
3609
+ let issuer;
3610
+ if (opts.disableJwtPlugin) issuer = ctx.context.baseURL;
3611
+ else try {
3612
+ issuer = getJwtPlugin(ctx.context).options?.jwt?.issuer ?? ctx.context.baseURL;
3613
+ } catch {
3614
+ issuer = ctx.context.baseURL;
3615
+ }
3616
+ return validateIssuerUrl(issuer);
3617
+ }
3618
+ /**
3619
+ * Error page url if redirect_uri has not been verified yet
3620
+ * Generates Url for custom error page
3621
+ */
3622
+ function getErrorURL(ctx, error, description) {
3623
+ return formatErrorURL(ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`, error, description);
3624
+ }
3625
+ async function authorizeEndpoint(ctx, opts, settings) {
3626
+ if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError$1("NOT_FOUND");
3627
+ if (!ctx.request) throw new APIError$1("UNAUTHORIZED", {
3628
+ error_description: "request not found",
3629
+ error: "invalid_request"
3630
+ });
3631
+ const query = ctx.query;
3632
+ await oAuthState.set({ query: query.toString() });
3633
+ if (!query.client_id) throw ctx.redirect(getErrorURL(ctx, "invalid_client", "client_id is required"));
3634
+ if (!query.response_type) throw ctx.redirect(getErrorURL(ctx, "invalid_request", "response_type is required"));
3635
+ const promptSet = ctx.query?.prompt ? parsePrompt(ctx.query?.prompt) : void 0;
3636
+ if (promptSet?.has("select_account") && !opts.selectAccount?.page) throw ctx.redirect(getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
3637
+ if (!(query.response_type === "code")) throw ctx.redirect(getErrorURL(ctx, "unsupported_response_type", "unsupported response type"));
3638
+ const client = await getClient(ctx, opts, query.client_id);
3639
+ if (!client) throw ctx.redirect(getErrorURL(ctx, "invalid_client", "client_id is required"));
3640
+ if (client.disabled) throw ctx.redirect(getErrorURL(ctx, "client_disabled", "client is disabled"));
3641
+ if (!client.redirectUris?.find((url) => url === query.redirect_uri) || !query.redirect_uri) throw ctx.redirect(getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
3642
+ let requestedScopes = query.scope?.split(" ").filter((s) => s);
3643
+ if (requestedScopes) {
3644
+ const validScopes = new Set(client.scopes ?? opts.scopes);
3645
+ const invalidScopes = requestedScopes.filter((scope) => {
3646
+ return !validScopes?.has(scope);
3647
+ });
3648
+ if (invalidScopes.length) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`, query.state, getIssuer(ctx, opts)));
3649
+ }
3650
+ if (!requestedScopes) {
3651
+ requestedScopes = client.scopes ?? opts.scopes ?? [];
3652
+ query.scope = requestedScopes.join(" ");
3653
+ }
3654
+ const pkceRequired = isPKCERequired(client, requestedScopes);
3655
+ if (pkceRequired) {
3656
+ if (!query.code_challenge || !query.code_challenge_method) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", pkceRequired.valueOf(), query.state, getIssuer(ctx, opts)));
3657
+ }
3658
+ if (query.code_challenge || query.code_challenge_method) {
3659
+ if (!query.code_challenge || !query.code_challenge_method) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", "code_challenge and code_challenge_method must both be provided", query.state, getIssuer(ctx, opts)));
3660
+ if (!["S256"].includes(query.code_challenge_method)) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method, only S256 is supported", query.state, getIssuer(ctx, opts)));
3661
+ }
3662
+ const session = await getSessionFromCtx(ctx);
3663
+ if (!session || promptSet?.has("login") || promptSet?.has("create")) return redirectWithPromptCode(ctx, opts, promptSet?.has("create") ? "create" : "login");
3664
+ if (settings?.isAuthorize && promptSet?.has("select_account")) return redirectWithPromptCode(ctx, opts, "select_account");
3665
+ if (settings?.isAuthorize && opts.selectAccount) {
3666
+ if (await opts.selectAccount.shouldRedirect({
3667
+ headers: ctx.request.headers,
3668
+ user: session.user,
3669
+ session: session.session,
3670
+ scopes: requestedScopes
3671
+ })) return redirectWithPromptCode(ctx, opts, "select_account");
3672
+ }
3673
+ if (opts.signup?.shouldRedirect) {
3674
+ const signupRedirect = await opts.signup.shouldRedirect({
3675
+ headers: ctx.request.headers,
3676
+ user: session.user,
3677
+ session: session.session,
3678
+ scopes: requestedScopes
3679
+ });
3680
+ if (signupRedirect) return redirectWithPromptCode(ctx, opts, "create", typeof signupRedirect === "string" ? signupRedirect : void 0);
3681
+ }
3682
+ if (!settings?.postLogin && opts.postLogin) {
3683
+ if (await opts.postLogin.shouldRedirect({
3684
+ headers: ctx.request.headers,
3685
+ user: session.user,
3686
+ session: session.session,
3687
+ scopes: requestedScopes
3688
+ })) return redirectWithPromptCode(ctx, opts, "post_login");
3689
+ }
3690
+ if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent");
3691
+ const referenceId = await opts.postLogin?.consentReferenceId?.({
3692
+ user: session.user,
3693
+ session: session.session,
3694
+ scopes: requestedScopes
3695
+ });
3696
+ if (client.skipConsent) return redirectWithAuthorizationCode(ctx, opts, {
3697
+ query,
3698
+ clientId: client.clientId,
3699
+ userId: session.user.id,
3700
+ sessionId: session.session.id,
3701
+ authTime: new Date(session.session.createdAt).getTime(),
3702
+ referenceId
3703
+ });
3704
+ const consent = await ctx.context.adapter.findOne({
3705
+ model: "oauthConsent",
3706
+ where: [
3707
+ {
3708
+ field: "clientId",
3709
+ value: client.clientId
3710
+ },
3711
+ {
3712
+ field: "userId",
3713
+ value: session.user.id
3714
+ },
3715
+ ...referenceId ? [{
3716
+ field: "referenceId",
3717
+ value: referenceId
3718
+ }] : []
3719
+ ]
3720
+ });
3721
+ if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val))) return redirectWithPromptCode(ctx, opts, "consent");
3722
+ return redirectWithAuthorizationCode(ctx, opts, {
3723
+ query,
3724
+ clientId: client.clientId,
3725
+ userId: session.user.id,
3726
+ sessionId: session.session.id,
3727
+ authTime: new Date(session.session.createdAt).getTime(),
3728
+ referenceId
3729
+ });
3730
+ }
3731
+ async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
3732
+ const code = generateRandomString(32, "a-z", "A-Z", "0-9");
3733
+ const iat = Math.floor(Date.now() / 1e3);
3734
+ const exp = iat + (opts.codeExpiresIn ?? 600);
3735
+ const data = {
3736
+ identifier: await storeToken(opts.storeTokens, code, "authorization_code"),
3737
+ updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
3738
+ expiresAt: /* @__PURE__ */ new Date(exp * 1e3),
3739
+ value: JSON.stringify({
3740
+ type: "authorization_code",
3741
+ query: ctx.query,
3742
+ userId: verificationValue.userId,
3743
+ sessionId: verificationValue?.sessionId,
3744
+ referenceId: verificationValue.referenceId,
3745
+ authTime: verificationValue.authTime
3746
+ })
3747
+ };
3748
+ ctx.context.verification_id ? await ctx.context.internalAdapter.updateVerificationValue(ctx.context.verification_id, data) : await ctx.context.internalAdapter.createVerificationValue({
3749
+ ...data,
3750
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3)
3751
+ });
3752
+ const redirectUriWithCode = new URL(verificationValue.query.redirect_uri);
3753
+ redirectUriWithCode.searchParams.set("code", code);
3754
+ if (verificationValue.query.state) redirectUriWithCode.searchParams.set("state", verificationValue.query.state);
3755
+ redirectUriWithCode.searchParams.set("iss", getIssuer(ctx, opts));
3756
+ return handleRedirect(ctx, redirectUriWithCode.toString());
3757
+ }
3758
+ async function redirectWithPromptCode(ctx, opts, type, page) {
3759
+ const queryParams = await signParams(ctx, opts);
3760
+ let path = opts.loginPage;
3761
+ if (type === "select_account") path = opts.selectAccount?.page ?? opts.loginPage;
3762
+ else if (type === "post_login") {
3763
+ if (!opts.postLogin?.page) throw new APIError$1("INTERNAL_SERVER_ERROR", { error_description: "postLogin should have been defined" });
3764
+ path = opts.postLogin?.page;
3765
+ } else if (type === "consent") path = opts.consentPage;
3766
+ else if (type === "create") path = opts.signup?.page ?? opts.loginPage;
3767
+ return handleRedirect(ctx, `${page ?? path}?${queryParams}`);
3768
+ }
3769
+ async function signParams(ctx, opts) {
3770
+ const exp = Math.floor(Date.now() / 1e3) + (opts.codeExpiresIn ?? 600);
3771
+ const params = new URLSearchParams(ctx.query);
3772
+ params.set("exp", String(exp));
3773
+ const signature = await makeSignature(params.toString(), ctx.context.secret);
3774
+ params.append("sig", signature);
3775
+ return params.toString();
3776
+ }
3777
+
3778
+ //#endregion
3779
+ //#region src/metadata.ts
3780
+ function authServerMetadata(ctx, opts, overrides) {
3781
+ const baseURL = ctx.context.baseURL;
3782
+ return {
3783
+ scopes_supported: overrides?.scopes_supported,
3784
+ issuer: validateIssuerUrl(opts?.jwt?.issuer ?? baseURL),
3785
+ authorization_endpoint: `${baseURL}/oauth2/authorize`,
3786
+ token_endpoint: `${baseURL}/oauth2/token`,
3787
+ jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
3788
+ registration_endpoint: `${baseURL}/oauth2/register`,
3789
+ introspection_endpoint: `${baseURL}/oauth2/introspect`,
3790
+ revocation_endpoint: `${baseURL}/oauth2/revoke`,
3791
+ response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
3792
+ response_modes_supported: ["query"],
3793
+ grant_types_supported: overrides?.grant_types_supported ?? [
3794
+ "authorization_code",
3795
+ "client_credentials",
3796
+ "refresh_token"
3797
+ ],
3798
+ token_endpoint_auth_methods_supported: [
3799
+ ...overrides?.public_client_supported ? ["none"] : [],
3800
+ "client_secret_basic",
3801
+ "client_secret_post"
3802
+ ],
3803
+ introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
3804
+ revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
3805
+ code_challenge_methods_supported: ["S256"],
3806
+ authorization_response_iss_parameter_supported: true
3807
+ };
3808
+ }
3809
+ function oidcServerMetadata(ctx, opts) {
3810
+ const baseURL = ctx.context.baseURL;
3811
+ const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
3812
+ return {
3813
+ ...authServerMetadata(ctx, jwtPluginOptions, {
3814
+ scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
3815
+ public_client_supported: opts.allowUnauthenticatedClientRegistration,
3816
+ grant_types_supported: opts.grantTypes,
3817
+ jwt_disabled: opts.disableJwtPlugin
3818
+ }),
3819
+ claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
3820
+ userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
3821
+ subject_types_supported: ["public"],
3822
+ id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
3823
+ end_session_endpoint: `${baseURL}/oauth2/end-session`,
3824
+ acr_values_supported: ["urn:mace:incommon:iap:bronze"],
3825
+ prompt_values_supported: [
3826
+ "login",
3827
+ "consent",
3828
+ "create",
3829
+ "select_account"
3830
+ ]
3831
+ };
3832
+ }
3833
+ /**
3834
+ * Provides an exportable `/.well-known/oauth-authorization-server`.
3835
+ *
3836
+ * Useful when basePath prevents the endpoint from being located at the root
3837
+ * and must be provided manually.
3838
+ *
3839
+ * @external
3840
+ */
3841
+ const oauthProviderAuthServerMetadata = (auth, opts) => {
3842
+ return async (_request) => {
3843
+ const res = await auth.api.getOAuthServerConfig();
3844
+ return new Response(JSON.stringify(res), {
3845
+ status: 200,
3846
+ headers: {
3847
+ "Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
3848
+ ...opts?.headers,
3849
+ "Content-Type": "application/json"
3850
+ }
3851
+ });
3852
+ };
3853
+ };
3854
+ /**
3855
+ * Provides an exportable `/.well-known/openid-configuration`.
3856
+ *
3857
+ * Useful when basePath prevents the endpoint from being located at the root
3858
+ * and must be provided manually.
3859
+ *
3860
+ * @external
3861
+ */
3862
+ const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
3863
+ return async (_request) => {
3864
+ const res = await auth.api.getOpenIdConfig();
3865
+ return new Response(JSON.stringify(res), {
3866
+ status: 200,
3867
+ headers: {
3868
+ "Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
3869
+ ...opts?.headers,
3870
+ "Content-Type": "application/json"
3871
+ }
3872
+ });
3719
3873
  };
3720
3874
  };
3721
3875
 
3722
3876
  //#endregion
3723
- export { authServerMetadata, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
3877
+ export { authServerMetadata, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
3878
+ //# sourceMappingURL=index.mjs.map