@better-auth/oauth-provider 1.4.17 → 1.4.19

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,124 +1,28 @@
1
- import { a as getJwtPlugin, c as parsePrompt, d as validateClientCredentials, i as getClient, l as storeClientSecret, n as decryptStoredClientSecret, p as mcpHandler, r as deleteFromPrompt, s as getStoredToken, t as basicToClientCredentials, u as storeToken } from "./utils-LksXOM6z.mjs";
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-CThxvHKd.mjs";
2
2
  import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
3
3
  import { APIError } from "better-call";
4
- import { BetterAuthError } from "@better-auth/core/error";
4
+ import { APIError as APIError$1, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
5
5
  import { constantTimeEqual, generateRandomString, makeSignature } from "better-auth/crypto";
6
+ import { BetterAuthError } from "@better-auth/core/error";
6
7
  import { defineRequestState } from "@better-auth/core/context";
7
8
  import { logger } from "@better-auth/core/env";
8
- import { APIError as APIError$1, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
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
15
  //#region src/authorize.ts
113
16
  /**
114
17
  * Formats an error url
115
18
  */
116
- function formatErrorURL(url, error, description, state) {
19
+ function formatErrorURL(url, error, description, state, iss) {
117
20
  const searchParams = new URLSearchParams({
118
21
  error,
119
22
  error_description: description
120
23
  });
121
24
  state && searchParams.append("state", state);
25
+ iss && searchParams.append("iss", iss);
122
26
  return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
123
27
  }
124
28
  const handleRedirect = (ctx, uri) => {
@@ -129,6 +33,39 @@ const handleRedirect = (ctx, uri) => {
129
33
  else throw ctx.redirect(uri);
130
34
  };
131
35
  /**
36
+ * Validates that the issuer URL
37
+ * - MUST use HTTPS scheme (HTTP allowed for localhost in dev)
38
+ * - MUST NOT contain query components
39
+ * - MUST NOT contain fragment components
40
+ *
41
+ * @returns The validated issuer URL, or a sanitized version if invalid
42
+ */
43
+ function validateIssuerUrl(issuer) {
44
+ try {
45
+ const url = new URL(issuer);
46
+ const isLocalhost$1 = url.hostname === "localhost" || url.hostname === "127.0.0.1";
47
+ if (url.protocol !== "https:" && !isLocalhost$1) url.protocol = "https:";
48
+ url.search = "";
49
+ url.hash = "";
50
+ return url.toString().replace(/\/$/, "");
51
+ } catch {
52
+ return issuer;
53
+ }
54
+ }
55
+ /**
56
+ * Gets the issuer identifier
57
+ */
58
+ function getIssuer(ctx, opts) {
59
+ let issuer;
60
+ if (opts.disableJwtPlugin) issuer = ctx.context.baseURL;
61
+ else try {
62
+ issuer = getJwtPlugin(ctx.context).options?.jwt?.issuer ?? ctx.context.baseURL;
63
+ } catch {
64
+ issuer = ctx.context.baseURL;
65
+ }
66
+ return validateIssuerUrl(issuer);
67
+ }
68
+ /**
132
69
  * Error page url if redirect_uri has not been verified yet
133
70
  * Generates Url for custom error page
134
71
  */
@@ -157,14 +94,14 @@ async function authorizeEndpoint(ctx, opts, settings) {
157
94
  const invalidScopes = requestedScopes.filter((scope) => {
158
95
  return !validScopes?.has(scope) || scope === "offline_access" && (query.code_challenge_method !== "S256" || !query.code_challenge);
159
96
  });
160
- if (invalidScopes.length) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`, query.state));
97
+ if (invalidScopes.length) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`, query.state, getIssuer(ctx, opts)));
161
98
  }
162
99
  if (!requestedScopes) {
163
100
  requestedScopes = client.scopes ?? opts.scopes ?? [];
164
101
  query.scope = requestedScopes.join(" ");
165
102
  }
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));
103
+ if (!query.code_challenge || !query.code_challenge_method) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", "pkce is required", query.state, getIssuer(ctx, opts)));
104
+ if (!["S256"].includes(query.code_challenge_method)) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method", query.state, getIssuer(ctx, opts)));
168
105
  const session = await getSessionFromCtx(ctx);
169
106
  if (!session || promptSet?.has("login") || promptSet?.has("create")) return redirectWithPromptCode(ctx, opts, promptSet?.has("create") ? "create" : "login");
170
107
  if (settings?.isAuthorize && promptSet?.has("select_account")) return redirectWithPromptCode(ctx, opts, "select_account");
@@ -255,6 +192,7 @@ async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
255
192
  const redirectUriWithCode = new URL(verificationValue.query.redirect_uri);
256
193
  redirectUriWithCode.searchParams.set("code", code);
257
194
  if (verificationValue.query.state) redirectUriWithCode.searchParams.set("state", verificationValue.query.state);
195
+ redirectUriWithCode.searchParams.set("iss", getIssuer(ctx, opts));
258
196
  return handleRedirect(ctx, redirectUriWithCode.toString());
259
197
  }
260
198
  async function redirectWithPromptCode(ctx, opts, type, page) {
@@ -277,6 +215,104 @@ async function signParams(ctx, opts) {
277
215
  return params.toString();
278
216
  }
279
217
 
218
+ //#endregion
219
+ //#region src/metadata.ts
220
+ function authServerMetadata(ctx, opts, overrides) {
221
+ const baseURL = ctx.context.baseURL;
222
+ return {
223
+ scopes_supported: overrides?.scopes_supported,
224
+ issuer: validateIssuerUrl(opts?.jwt?.issuer ?? baseURL),
225
+ authorization_endpoint: `${baseURL}/oauth2/authorize`,
226
+ token_endpoint: `${baseURL}/oauth2/token`,
227
+ jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
228
+ registration_endpoint: `${baseURL}/oauth2/register`,
229
+ introspection_endpoint: `${baseURL}/oauth2/introspect`,
230
+ revocation_endpoint: `${baseURL}/oauth2/revoke`,
231
+ response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
232
+ response_modes_supported: ["query"],
233
+ grant_types_supported: overrides?.grant_types_supported ?? [
234
+ "authorization_code",
235
+ "client_credentials",
236
+ "refresh_token"
237
+ ],
238
+ token_endpoint_auth_methods_supported: [
239
+ ...overrides?.public_client_supported ? ["none"] : [],
240
+ "client_secret_basic",
241
+ "client_secret_post"
242
+ ],
243
+ introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
244
+ revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
245
+ code_challenge_methods_supported: ["S256"],
246
+ authorization_response_iss_parameter_supported: true
247
+ };
248
+ }
249
+ function oidcServerMetadata(ctx, opts) {
250
+ const baseURL = ctx.context.baseURL;
251
+ const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
252
+ return {
253
+ ...authServerMetadata(ctx, jwtPluginOptions, {
254
+ scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
255
+ public_client_supported: opts.allowUnauthenticatedClientRegistration,
256
+ grant_types_supported: opts.grantTypes,
257
+ jwt_disabled: opts.disableJwtPlugin
258
+ }),
259
+ claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
260
+ userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
261
+ subject_types_supported: ["public"],
262
+ id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
263
+ end_session_endpoint: `${baseURL}/oauth2/end-session`,
264
+ acr_values_supported: ["urn:mace:incommon:iap:bronze"],
265
+ prompt_values_supported: [
266
+ "login",
267
+ "consent",
268
+ "create",
269
+ "select_account"
270
+ ]
271
+ };
272
+ }
273
+ /**
274
+ * Provides an exportable `/.well-known/oauth-authorization-server`.
275
+ *
276
+ * Useful when basePath prevents the endpoint from being located at the root
277
+ * and must be provided manually.
278
+ *
279
+ * @external
280
+ */
281
+ const oauthProviderAuthServerMetadata = (auth, opts) => {
282
+ return async (_request) => {
283
+ const res = await auth.api.getOAuthServerConfig();
284
+ return new Response(JSON.stringify(res), {
285
+ status: 200,
286
+ headers: {
287
+ "Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
288
+ ...opts?.headers,
289
+ "Content-Type": "application/json"
290
+ }
291
+ });
292
+ };
293
+ };
294
+ /**
295
+ * Provides an exportable `/.well-known/openid-configuration`.
296
+ *
297
+ * Useful when basePath prevents the endpoint from being located at the root
298
+ * and must be provided manually.
299
+ *
300
+ * @external
301
+ */
302
+ const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
303
+ return async (_request) => {
304
+ const res = await auth.api.getOpenIdConfig();
305
+ return new Response(JSON.stringify(res), {
306
+ status: 200,
307
+ headers: {
308
+ "Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
309
+ ...opts?.headers,
310
+ "Content-Type": "application/json"
311
+ }
312
+ });
313
+ };
314
+ };
315
+
280
316
  //#endregion
281
317
  //#region src/consent.ts
282
318
  async function consentEndpoint(ctx, opts) {
@@ -301,7 +337,7 @@ async function consentEndpoint(ctx, opts) {
301
337
  }
302
338
  if (!(ctx.body.accept === true)) return {
303
339
  redirect: true,
304
- uri: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0)
340
+ url: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0, getIssuer(ctx, opts))
305
341
  };
306
342
  const session = await getSessionFromCtx(ctx);
307
343
  const referenceId = await opts.postLogin?.consentReferenceId?.({
@@ -358,7 +394,7 @@ async function consentEndpoint(ctx, opts) {
358
394
  const { url } = await authorizeEndpoint(ctx, opts);
359
395
  return {
360
396
  redirect: true,
361
- uri: url
397
+ url
362
398
  };
363
399
  }
364
400
 
@@ -384,7 +420,7 @@ async function selected(ctx, opts) {
384
420
  const { url } = await authorizeEndpoint(ctx, opts);
385
421
  return {
386
422
  redirect: true,
387
- uri: url
423
+ url
388
424
  };
389
425
  }
390
426
  async function created(ctx, opts) {
@@ -397,7 +433,7 @@ async function created(ctx, opts) {
397
433
  const { url } = await authorizeEndpoint(ctx, opts);
398
434
  return {
399
435
  redirect: true,
400
- uri: url
436
+ url
401
437
  };
402
438
  }
403
439
  async function postLogin(ctx, opts) {
@@ -412,7 +448,7 @@ async function postLogin(ctx, opts) {
412
448
  const { url } = await authorizeEndpoint(ctx, opts, { postLogin: true });
413
449
  return {
414
450
  redirect: true,
415
- uri: url
451
+ url
416
452
  };
417
453
  }
418
454
 
@@ -516,7 +552,7 @@ async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, r
516
552
  scopes,
517
553
  resource: ctx.body.resource,
518
554
  referenceId,
519
- metadata: client.metadata ? JSON.parse(client.metadata) : void 0
555
+ metadata: parseClientMetadata(client.metadata)
520
556
  }) : {};
521
557
  const jwtPluginOptions = getJwtPlugin(ctx.context).options;
522
558
  return signJWT(ctx, {
@@ -547,7 +583,7 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId)
547
583
  const customClaims = opts.customIdTokenClaims ? await opts.customIdTokenClaims({
548
584
  user,
549
585
  scopes,
550
- metadata: client.metadata ? JSON.parse(client.metadata) : void 0
586
+ metadata: parseClientMetadata(client.metadata)
551
587
  }) : {};
552
588
  const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
553
589
  const payload = {
@@ -857,7 +893,7 @@ async function handleClientCredentialsGrant(ctx, opts) {
857
893
  const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
858
894
  scopes: requestedScopes,
859
895
  resource: ctx.body.resource,
860
- metadata: client.metadata ? JSON.parse(client.metadata) : void 0
896
+ metadata: parseClientMetadata(client.metadata)
861
897
  }) : {};
862
898
  const accessToken = audience && !opts.disableJwtPlugin ? await signJWT(ctx, {
863
899
  options: jwtPluginOptions,
@@ -1069,7 +1105,7 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
1069
1105
  user,
1070
1106
  scopes: accessToken.scopes,
1071
1107
  referenceId: accessToken?.referenceId,
1072
- metadata: client?.metadata ? JSON.parse(client.metadata) : void 0
1108
+ metadata: parseClientMetadata(client?.metadata)
1073
1109
  }) : {};
1074
1110
  const jwtPluginOptions = (opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options;
1075
1111
  return {
@@ -1079,8 +1115,8 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
1079
1115
  client_id: accessToken.clientId,
1080
1116
  sub: user?.id,
1081
1117
  sid: sessionId,
1082
- exp: Math.floor(accessToken.expiresAt.getTime() / 1e3),
1083
- iat: Math.floor(accessToken.createdAt.getTime() / 1e3),
1118
+ exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
1119
+ iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
1084
1120
  scope: accessToken.scopes?.join(" ")
1085
1121
  };
1086
1122
  }
@@ -1123,8 +1159,8 @@ async function validateRefreshToken(ctx, opts, token, clientId) {
1123
1159
  iss: ((opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options)?.jwt?.issuer ?? ctx.context.baseURL,
1124
1160
  sub: user?.id,
1125
1161
  sid: sessionId,
1126
- exp: Math.floor(refreshToken.expiresAt.getTime() / 1e3),
1127
- iat: Math.floor(refreshToken.createdAt.getTime() / 1e3),
1162
+ exp: Math.floor(new Date(refreshToken.expiresAt).getTime() / 1e3),
1163
+ iat: Math.floor(new Date(refreshToken.createdAt).getTime() / 1e3),
1128
1164
  scope: refreshToken.scopes?.join(" ")
1129
1165
  };
1130
1166
  }
@@ -1400,7 +1436,11 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1400
1436
  });
1401
1437
  const client = await ctx.context.adapter.create({
1402
1438
  model: "oauthClient",
1403
- data: schema$1
1439
+ data: {
1440
+ ...schema$1,
1441
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3),
1442
+ updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
1443
+ }
1404
1444
  });
1405
1445
  return ctx.json(schemaToOAuth({
1406
1446
  ...client,
@@ -1420,14 +1460,19 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1420
1460
  * @returns
1421
1461
  */
1422
1462
  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, ...rest } = input;
1463
+ 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;
1424
1464
  const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
1425
1465
  const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
1466
+ const scopes = _scope?.split(" ");
1467
+ const metadataObj = {
1468
+ ...rest && Object.keys(rest).length ? rest : {},
1469
+ ...inputMetadata && typeof inputMetadata === "object" ? inputMetadata : {}
1470
+ };
1426
1471
  return {
1427
1472
  clientId,
1428
1473
  clientSecret,
1429
1474
  disabled,
1430
- scopes: _scope?.split(" "),
1475
+ scopes,
1431
1476
  userId,
1432
1477
  createdAt,
1433
1478
  expiresAt,
@@ -1450,7 +1495,7 @@ function oauthToSchema(input) {
1450
1495
  skipConsent,
1451
1496
  enableEndSession,
1452
1497
  referenceId,
1453
- metadata: rest && Object.keys(rest).length ? JSON.stringify(rest) : void 0
1498
+ metadata: Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0
1454
1499
  };
1455
1500
  }
1456
1501
  /**
@@ -1461,11 +1506,11 @@ function oauthToSchema(input) {
1461
1506
  */
1462
1507
  function schemaToOAuth(input) {
1463
1508
  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;
1464
- const _expiresAt = expiresAt ? Math.round(expiresAt.getTime() / 1e3) : void 0;
1465
- const _createdAt = createdAt ? Math.round(createdAt.getTime() / 1e3) : void 0;
1509
+ const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
1510
+ const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
1466
1511
  const _scopes = scopes?.join(" ");
1467
1512
  return {
1468
- ...metadata ? JSON.parse(metadata) : void 0,
1513
+ ...parseClientMetadata(metadata),
1469
1514
  client_id: clientId,
1470
1515
  client_secret: clientSecret ?? void 0,
1471
1516
  client_secret_expires_at: clientSecret ? _expiresAt ?? 0 : void 0,
@@ -1497,8 +1542,19 @@ function schemaToOAuth(input) {
1497
1542
 
1498
1543
  //#endregion
1499
1544
  //#region src/types/zod.ts
1545
+ const DANGEROUS_SCHEMES = [
1546
+ "javascript:",
1547
+ "data:",
1548
+ "vbscript:"
1549
+ ];
1550
+ function isLocalhost(hostname) {
1551
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
1552
+ }
1500
1553
  /**
1501
- * Reusable URL validation that disallows javascript: scheme
1554
+ * Reusable URL validation for OAuth redirect URIs.
1555
+ * - Blocks dangerous schemes (javascript:, data:, vbscript:)
1556
+ * - For http/https: requires HTTPS (HTTP allowed only for localhost)
1557
+ * - Allows custom schemes for mobile apps (e.g., myapp://callback)
1502
1558
  */
1503
1559
  const SafeUrlSchema = z.url().superRefine((val, ctx) => {
1504
1560
  if (!URL.canParse(val)) {
@@ -1509,19 +1565,30 @@ const SafeUrlSchema = z.url().superRefine((val, ctx) => {
1509
1565
  });
1510
1566
  return z.NEVER;
1511
1567
  }
1512
- }).refine((url) => {
1513
- const u = new URL(url);
1514
- return u.protocol !== "javascript:" && u.protocol !== "data:" && u.protocol !== "vbscript:";
1515
- }, { message: "URL cannot use javascript:, data:, or vbscript: scheme" });
1568
+ const u = new URL(val);
1569
+ if (DANGEROUS_SCHEMES.includes(u.protocol)) {
1570
+ ctx.addIssue({
1571
+ code: "custom",
1572
+ message: "URL cannot use javascript:, data:, or vbscript: scheme"
1573
+ });
1574
+ return;
1575
+ }
1576
+ if (u.protocol === "http:" || u.protocol === "https:") {
1577
+ if (u.protocol === "http:" && !isLocalhost(u.hostname)) ctx.addIssue({
1578
+ code: "custom",
1579
+ message: "Redirect URI must use HTTPS (HTTP allowed only for localhost)"
1580
+ });
1581
+ }
1582
+ });
1516
1583
 
1517
1584
  //#endregion
1518
1585
  //#region src/oauthClient/endpoints.ts
1519
1586
  async function getClientEndpoint(ctx, opts) {
1520
1587
  const session = await getSessionFromCtx(ctx);
1521
1588
  if (!session) throw new APIError$1("UNAUTHORIZED");
1522
- if (!ctx.request) throw new APIError$1("BAD_REQUEST");
1589
+ if (!ctx.headers) throw new APIError$1("BAD_REQUEST");
1523
1590
  if (opts.clientPrivileges && !await opts.clientPrivileges({
1524
- headers: ctx.request.headers,
1591
+ headers: ctx.headers,
1525
1592
  action: "read",
1526
1593
  session: session.session,
1527
1594
  user: session.user
@@ -1567,9 +1634,9 @@ async function getClientPublicEndpoint(ctx, opts) {
1567
1634
  async function getClientsEndpoint(ctx, opts) {
1568
1635
  const session = await getSessionFromCtx(ctx);
1569
1636
  if (!session) throw new APIError$1("UNAUTHORIZED");
1570
- if (!ctx.request) throw new APIError$1("BAD_REQUEST");
1637
+ if (!ctx.headers) throw new APIError$1("BAD_REQUEST");
1571
1638
  if (opts.clientPrivileges && !await opts.clientPrivileges({
1572
- headers: ctx.request.headers,
1639
+ headers: ctx.headers,
1573
1640
  action: "list",
1574
1641
  session: session.session,
1575
1642
  user: session.user
@@ -1608,9 +1675,9 @@ async function getClientsEndpoint(ctx, opts) {
1608
1675
  async function deleteClientEndpoint(ctx, opts) {
1609
1676
  const session = await getSessionFromCtx(ctx);
1610
1677
  if (!session) throw new APIError$1("UNAUTHORIZED");
1611
- if (!ctx.request) throw new APIError$1("BAD_REQUEST");
1678
+ if (!ctx.headers) throw new APIError$1("BAD_REQUEST");
1612
1679
  if (opts.clientPrivileges && !await opts.clientPrivileges({
1613
- headers: ctx.request.headers,
1680
+ headers: ctx.headers,
1614
1681
  action: "delete",
1615
1682
  session: session.session,
1616
1683
  user: session.user
@@ -1641,9 +1708,9 @@ async function deleteClientEndpoint(ctx, opts) {
1641
1708
  async function updateClientEndpoint(ctx, opts) {
1642
1709
  const session = await getSessionFromCtx(ctx);
1643
1710
  if (!session) throw new APIError$1("UNAUTHORIZED");
1644
- if (!ctx.request) throw new APIError$1("BAD_REQUEST");
1711
+ if (!ctx.headers) throw new APIError$1("BAD_REQUEST");
1645
1712
  if (opts.clientPrivileges && !await opts.clientPrivileges({
1646
- headers: ctx.request.headers,
1713
+ headers: ctx.headers,
1647
1714
  action: "update",
1648
1715
  session: session.session,
1649
1716
  user: session.user
@@ -1679,7 +1746,10 @@ async function updateClientEndpoint(ctx, opts) {
1679
1746
  field: "clientId",
1680
1747
  value: clientId
1681
1748
  }],
1682
- update: oauthToSchema(updates)
1749
+ update: {
1750
+ ...oauthToSchema(updates),
1751
+ updatedAt: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3)
1752
+ }
1683
1753
  });
1684
1754
  if (!updatedClient) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1685
1755
  error_description: "unable to update client",
@@ -1692,9 +1762,9 @@ async function updateClientEndpoint(ctx, opts) {
1692
1762
  async function rotateClientSecretEndpoint(ctx, opts) {
1693
1763
  const session = await getSessionFromCtx(ctx);
1694
1764
  if (!session) throw new APIError$1("UNAUTHORIZED");
1695
- if (!ctx.request) throw new APIError$1("BAD_REQUEST");
1765
+ if (!ctx.headers) throw new APIError$1("BAD_REQUEST");
1696
1766
  if (opts.clientPrivileges && !await opts.clientPrivileges({
1697
- headers: ctx.request.headers,
1767
+ headers: ctx.headers,
1698
1768
  action: "rotate",
1699
1769
  session: session.session,
1700
1770
  user: session.user
@@ -1727,8 +1797,8 @@ async function rotateClientSecretEndpoint(ctx, opts) {
1727
1797
  value: clientId
1728
1798
  }],
1729
1799
  update: {
1730
- ...schemaToOAuth(client),
1731
- clientSecret: storedClientSecret
1800
+ clientSecret: storedClientSecret,
1801
+ updatedAt: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3)
1732
1802
  }
1733
1803
  });
1734
1804
  if (!updatedClient) throw new APIError$1("INTERNAL_SERVER_ERROR", {
@@ -2915,7 +2985,12 @@ const oauthProvider = (options) => {
2915
2985
  metadata: { SERVER_ONLY: true }
2916
2986
  }, async (ctx) => {
2917
2987
  if (opts.scopes && opts.scopes.includes("openid")) return oidcServerMetadata(ctx, opts);
2918
- else return authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options, { scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes });
2988
+ else return authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options, {
2989
+ scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
2990
+ public_client_supported: opts.allowUnauthenticatedClientRegistration,
2991
+ grant_types_supported: opts.grantTypes,
2992
+ jwt_disabled: opts.disableJwtPlugin
2993
+ });
2919
2994
  }),
2920
2995
  getOpenIdConfig: createAuthEndpoint("/.well-known/openid-configuration", {
2921
2996
  method: "GET",
@@ -3710,9 +3785,42 @@ const oauthProvider = (options) => {
3710
3785
  updateOAuthConsent: updateOAuthConsent(opts),
3711
3786
  deleteOAuthConsent: deleteOAuthConsent(opts)
3712
3787
  },
3713
- schema: mergeSchema(schema, opts?.schema)
3788
+ schema: mergeSchema(schema, opts?.schema),
3789
+ rateLimit: [
3790
+ ...opts.rateLimit?.token !== false ? [{
3791
+ pathMatcher: (path) => path === "/oauth2/token",
3792
+ window: opts.rateLimit?.token?.window ?? 60,
3793
+ max: opts.rateLimit?.token?.max ?? 20
3794
+ }] : [],
3795
+ ...opts.rateLimit?.authorize !== false ? [{
3796
+ pathMatcher: (path) => path === "/oauth2/authorize",
3797
+ window: opts.rateLimit?.authorize?.window ?? 60,
3798
+ max: opts.rateLimit?.authorize?.max ?? 30
3799
+ }] : [],
3800
+ ...opts.rateLimit?.introspect !== false ? [{
3801
+ pathMatcher: (path) => path === "/oauth2/introspect",
3802
+ window: opts.rateLimit?.introspect?.window ?? 60,
3803
+ max: opts.rateLimit?.introspect?.max ?? 100
3804
+ }] : [],
3805
+ ...opts.rateLimit?.revoke !== false ? [{
3806
+ pathMatcher: (path) => path === "/oauth2/revoke",
3807
+ window: opts.rateLimit?.revoke?.window ?? 60,
3808
+ max: opts.rateLimit?.revoke?.max ?? 30
3809
+ }] : [],
3810
+ ...opts.rateLimit?.register !== false ? [{
3811
+ pathMatcher: (path) => path === "/oauth2/register",
3812
+ window: opts.rateLimit?.register?.window ?? 60,
3813
+ max: opts.rateLimit?.register?.max ?? 5
3814
+ }] : [],
3815
+ ...opts.rateLimit?.userinfo !== false ? [{
3816
+ pathMatcher: (path) => path === "/oauth2/userinfo",
3817
+ window: opts.rateLimit?.userinfo?.window ?? 60,
3818
+ max: opts.rateLimit?.userinfo?.max ?? 60
3819
+ }] : []
3820
+ ]
3714
3821
  };
3715
3822
  };
3716
3823
 
3717
3824
  //#endregion
3718
- export { authServerMetadata, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
3825
+ export { authServerMetadata, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
3826
+ //# sourceMappingURL=index.mjs.map