@better-auth/oauth-provider 1.6.3 → 1.7.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,8 +1,11 @@
1
- import { _ as validateClientCredentials, a as getJwtPlugin, b as mcpHandler, c as isPKCERequired, d as parsePrompt, f as resolveSessionAuthTime, g as storeToken, h as storeClientSecret, i as getClient, l as normalizeTimestampValue, m as searchParamsToQuery, n as decryptStoredClientSecret, p as resolveSubjectIdentifier, r as deleteFromPrompt, s as getStoredToken, t as basicToClientCredentials, u as parseClientMetadata, v as verifyOAuthQueryParams } from "./utils-B9Pj9EPf.mjs";
2
- import { t as PACKAGE_VERSION } from "./version-BlcZ64XB.mjs";
1
+ import { n as isPrivateHostname } from "./client-assertion-CderPEmR.mjs";
2
+ import { n as mcpHandler } from "./mcp-CYnz-MXn.mjs";
3
+ import { _ as storeClientSecret, a as getClient, b as validateClientCredentials, c as getStoredToken, d as normalizeTimestampValue, f as parseClientMetadata, g as searchParamsToQuery, h as resolveSubjectIdentifier, i as extractClientCredentials, l as isPKCERequired, m as resolveSessionAuthTime, n as deleteFromPrompt, o as getJwtPlugin, p as parsePrompt, r as destructureCredentials, t as decryptStoredClientSecret, u as mergeDiscoveryMetadata, v as storeToken, x as verifyOAuthQueryParams, y as toClientDiscoveryArray } from "./utils-Cx_XnD9i.mjs";
4
+ import { t as PACKAGE_VERSION } from "./version-DIwdpXrQ.mjs";
3
5
  import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
4
6
  import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
5
7
  import { APIError as APIError$1 } from "better-call";
8
+ import { ASSERTION_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
6
9
  import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
7
10
  import { generateRandomString, makeSignature } from "better-auth/crypto";
8
11
  import { defineRequestState } from "@better-auth/core/context";
@@ -11,8 +14,8 @@ import { BetterAuthError } from "@better-auth/core/error";
11
14
  import { parseSetCookieHeader } from "better-auth/cookies";
12
15
  import { mergeSchema } from "better-auth/db";
13
16
  import * as z from "zod";
14
- import { signJWT, toExpJWT } from "better-auth/plugins";
15
- import { SignJWT, compactVerify, createLocalJWKSet, decodeJwt } from "jose";
17
+ import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
18
+ import { SignJWT, base64url, compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader } from "jose";
16
19
  //#region src/consent.ts
17
20
  async function consentEndpoint(ctx, opts) {
18
21
  const _query = (await oAuthState.get())?.query;
@@ -171,7 +174,7 @@ const oauthAuthorizationQuerySchema = z.object({
171
174
  request_uri: z.string().optional(),
172
175
  redirect_uri: z.string(),
173
176
  scope: z.string().optional(),
174
- state: z.string(),
177
+ state: z.string().optional(),
175
178
  client_id: z.string(),
176
179
  prompt: z.string().optional(),
177
180
  display: z.string().optional(),
@@ -352,10 +355,23 @@ async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, r
352
355
  });
353
356
  }
354
357
  /**
358
+ * Computes an OIDC hash (at_hash, c_hash) per OIDC Core §3.1.3.6.
359
+ * Hashes the token, takes the left half, and base64url-encodes it.
360
+ */
361
+ async function computeOidcHash(token, signingAlg) {
362
+ let hashAlg;
363
+ if (signingAlg === "EdDSA") hashAlg = "SHA-512";
364
+ else if (signingAlg.endsWith("384")) hashAlg = "SHA-384";
365
+ else if (signingAlg.endsWith("512")) hashAlg = "SHA-512";
366
+ else hashAlg = "SHA-256";
367
+ const digest = new Uint8Array(await crypto.subtle.digest(hashAlg, new TextEncoder().encode(token)));
368
+ return base64url.encode(digest.slice(0, digest.length / 2));
369
+ }
370
+ /**
355
371
  * Creates a user id token in code_authorization with scope of 'openid'
356
372
  * and hybrid/implicit (not yet implemented) flows
357
373
  */
358
- async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime) {
374
+ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) {
359
375
  const iat = Math.floor(Date.now() / 1e3);
360
376
  const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
361
377
  const userClaims = userNormalClaims(user, scopes);
@@ -368,11 +384,15 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
368
384
  metadata: parseClientMetadata(client.metadata)
369
385
  }) : {};
370
386
  const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
387
+ const resolvedKey = !opts.disableJwtPlugin && !jwtPluginOptions?.jwt?.sign ? await resolveSigningKey(ctx, jwtPluginOptions) : void 0;
388
+ const signingAlg = opts.disableJwtPlugin ? "HS256" : resolvedKey?.alg ?? jwtPluginOptions?.jwks?.keyPairConfig?.alg;
389
+ const atHash = accessToken ? await computeOidcHash(accessToken, signingAlg) : void 0;
371
390
  const payload = {
372
391
  ...userClaims,
373
392
  auth_time: authTimeSec,
374
393
  acr,
375
394
  ...customClaims,
395
+ at_hash: atHash,
376
396
  iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
377
397
  sub: resolvedSub,
378
398
  aud: client.clientId,
@@ -382,10 +402,19 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
382
402
  sid: client.enableEndSession ? sessionId : void 0
383
403
  };
384
404
  if (opts.disableJwtPlugin && !client.clientSecret) return;
385
- return opts.disableJwtPlugin ? new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(new TextEncoder().encode(await decryptStoredClientSecret(ctx, opts.storeClientSecret, client.clientSecret))) : signJWT(ctx, {
405
+ const idToken = opts.disableJwtPlugin ? await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(new TextEncoder().encode(await decryptStoredClientSecret(ctx, opts.storeClientSecret, client.clientSecret))) : await signJWT(ctx, {
386
406
  options: jwtPluginOptions,
387
- payload
407
+ payload,
408
+ resolvedKey: resolvedKey ?? void 0
388
409
  });
410
+ if (idToken && atHash && jwtPluginOptions?.jwt?.sign) {
411
+ const header = decodeProtectedHeader(idToken);
412
+ if (header.alg !== signingAlg) throw new APIError("INTERNAL_SERVER_ERROR", {
413
+ error_description: `ID token signed with "${header.alg}" but at_hash was computed for "${signingAlg}". Ensure jwt.sign uses the algorithm declared in keyPairConfig.alg.`,
414
+ error: "server_error"
415
+ });
416
+ }
417
+ return idToken;
389
418
  }
390
419
  /**
391
420
  * Encodes a refresh token for a client
@@ -497,23 +526,20 @@ async function createUserTokens(ctx, opts, params) {
497
526
  exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
498
527
  sid: sessionId
499
528
  }, existingRefreshToken, authTime) : void 0;
500
- const [accessToken, refreshToken, idToken] = await Promise.all([
501
- isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, {
502
- iat,
503
- exp,
504
- sid: sessionId
505
- }) : createOpaqueAccessToken(ctx, opts, user, client, scopes, {
506
- iat,
507
- exp,
508
- sid: sessionId
509
- }, referenceId, earlyRefreshToken?.id),
510
- earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
511
- iat,
512
- exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
513
- sid: sessionId
514
- }, existingRefreshToken, authTime) : void 0,
515
- isIdToken && user ? createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime) : void 0
516
- ]);
529
+ const [accessToken, refreshToken] = await Promise.all([isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, {
530
+ iat,
531
+ exp,
532
+ sid: sessionId
533
+ }) : createOpaqueAccessToken(ctx, opts, user, client, scopes, {
534
+ iat,
535
+ exp,
536
+ sid: sessionId
537
+ }, referenceId, earlyRefreshToken?.id), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
538
+ iat,
539
+ exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
540
+ sid: sessionId
541
+ }, existingRefreshToken, authTime) : void 0]);
542
+ const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) : void 0;
517
543
  return ctx.json({
518
544
  ...customFields,
519
545
  access_token: accessToken,
@@ -569,13 +595,8 @@ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri)
569
595
  * Obtains new Session Jwt and Refresh Tokens using a code
570
596
  */
571
597
  async function handleAuthorizationCodeGrant(ctx, opts) {
572
- let { client_id, client_secret, code, code_verifier, redirect_uri } = ctx.body;
573
- const authorization = ctx.request?.headers.get("authorization") || null;
574
- if (authorization?.startsWith("Basic ")) {
575
- const res = basicToClientCredentials(authorization);
576
- client_id = res?.client_id;
577
- client_secret = res?.client_secret;
578
- }
598
+ const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
599
+ const { code, code_verifier, redirect_uri } = ctx.body;
579
600
  if (!client_id) throw new APIError("BAD_REQUEST", {
580
601
  error_description: "client_id is required",
581
602
  error: "invalid_request"
@@ -590,7 +611,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
590
611
  });
591
612
  const isAuthCodeWithSecret = client_id && client_secret;
592
613
  const isAuthCodeWithPkce = client_id && code && code_verifier;
593
- if (!isAuthCodeWithSecret && !isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
614
+ if (!isAuthCodeWithSecret && !isAuthCodeWithPkce && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
594
615
  error_description: "Either code_verifier or client_secret is required",
595
616
  error: "invalid_request"
596
617
  });
@@ -602,14 +623,14 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
602
623
  error: "invalid_scope"
603
624
  });
604
625
  /** Verify Client */
605
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes);
626
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerifiedClient);
606
627
  if (isPKCERequired(client, (verificationValue.query?.scope)?.split(" ") || [])) {
607
628
  if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
608
629
  error_description: "PKCE is required for this client",
609
630
  error: "invalid_request"
610
631
  });
611
- } else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret)) throw new APIError("BAD_REQUEST", {
612
- error_description: "Either PKCE (code_verifier) or client authentication (client_secret) is required",
632
+ } else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret || preVerifiedClient)) throw new APIError("BAD_REQUEST", {
633
+ error_description: "Either PKCE (code_verifier) or client authentication (client_secret or client_assertion) is required",
613
634
  error: "invalid_request"
614
635
  });
615
636
  /** Check PKCE challenge if verifier is provided */
@@ -670,22 +691,17 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
670
691
  * MUST follow https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
671
692
  */
672
693
  async function handleClientCredentialsGrant(ctx, opts) {
673
- let { client_id, client_secret, scope } = ctx.body;
674
- const authorization = ctx.request?.headers.get("authorization") || null;
675
- if (authorization?.startsWith("Basic ")) {
676
- const res = basicToClientCredentials(authorization);
677
- client_id = res?.client_id;
678
- client_secret = res?.client_secret;
679
- }
694
+ const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
695
+ const { scope } = ctx.body;
680
696
  if (!client_id) throw new APIError("BAD_REQUEST", {
681
697
  error_description: "Missing required client_id",
682
698
  error: "invalid_grant"
683
699
  });
684
- if (!client_secret) throw new APIError("BAD_REQUEST", {
700
+ if (!client_secret && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
685
701
  error_description: "Missing a required client_secret",
686
702
  error: "invalid_grant"
687
703
  });
688
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
704
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
689
705
  let requestedScopes = scope?.split(" ");
690
706
  if (requestedScopes) {
691
707
  const validScopes = new Set(client.scopes ?? opts.scopes);
@@ -717,13 +733,8 @@ async function handleClientCredentialsGrant(ctx, opts) {
717
733
  * To add scopes, you must restart the authorize process again.
718
734
  */
719
735
  async function handleRefreshTokenGrant(ctx, opts) {
720
- let { client_id, client_secret, refresh_token, scope } = ctx.body;
721
- const authorization = ctx.request?.headers.get("authorization") || null;
722
- if (authorization?.startsWith("Basic ")) {
723
- const res = basicToClientCredentials(authorization);
724
- client_id = res?.client_id;
725
- client_secret = res?.client_secret;
726
- }
736
+ const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
737
+ const { refresh_token, scope } = ctx.body;
727
738
  if (!client_id) throw new APIError("BAD_REQUEST", {
728
739
  error_description: "Missing required client_id",
729
740
  error: "invalid_grant"
@@ -777,7 +788,7 @@ async function handleRefreshTokenGrant(ctx, opts) {
777
788
  error: "invalid_scope"
778
789
  });
779
790
  }
780
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes);
791
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes, preVerifiedClient);
781
792
  const user = await ctx.context.internalAdapter.findUserById(refreshToken.userId);
782
793
  if (!user) throw new APIError("BAD_REQUEST", {
783
794
  error_description: "user not found",
@@ -1004,14 +1015,9 @@ async function resolveIntrospectionSub(opts, payload, client) {
1004
1015
  return payload;
1005
1016
  }
1006
1017
  async function introspectEndpoint(ctx, opts) {
1007
- let { client_id, client_secret, token, token_type_hint } = ctx.body;
1008
- const authorization = ctx.request?.headers.get("authorization") || null;
1009
- if (authorization?.startsWith("Basic ")) {
1010
- const res = basicToClientCredentials(authorization);
1011
- client_id = res?.client_id;
1012
- client_secret = res?.client_secret;
1013
- }
1014
- if (!client_id || !client_secret) throw new APIError$1("UNAUTHORIZED", {
1018
+ let { token, token_type_hint } = ctx.body;
1019
+ const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
1020
+ if (!client_id || !client_secret && !preVerifiedClient) throw new APIError$1("UNAUTHORIZED", {
1015
1021
  error_description: "missing required credentials",
1016
1022
  error: "invalid_client"
1017
1023
  });
@@ -1020,7 +1026,7 @@ async function introspectEndpoint(ctx, opts) {
1020
1026
  error_description: "missing a required token for introspection",
1021
1027
  error: "invalid_request"
1022
1028
  });
1023
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
1029
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
1024
1030
  try {
1025
1031
  if (token_type_hint === void 0 || token_type_hint === "access_token") try {
1026
1032
  return resolveIntrospectionSub(opts, await validateAccessToken(ctx, opts, token, client.clientId), client);
@@ -1264,14 +1270,59 @@ async function checkOAuthClient(client, opts, settings) {
1264
1270
  error: "invalid_client_metadata",
1265
1271
  error_description: `pkce is required for registered clients.`
1266
1272
  });
1273
+ if (client.token_endpoint_auth_method === "private_key_jwt") {
1274
+ if (client.jwks && client.jwks_uri) throw new APIError("BAD_REQUEST", {
1275
+ error: "invalid_client_metadata",
1276
+ error_description: "jwks and jwks_uri are mutually exclusive"
1277
+ });
1278
+ if (!client.jwks && !client.jwks_uri) throw new APIError("BAD_REQUEST", {
1279
+ error: "invalid_client_metadata",
1280
+ error_description: "private_key_jwt requires either jwks or jwks_uri"
1281
+ });
1282
+ if (client.jwks_uri) try {
1283
+ const uri = new URL(client.jwks_uri);
1284
+ if (uri.protocol !== "https:") throw new APIError("BAD_REQUEST", {
1285
+ error: "invalid_client_metadata",
1286
+ error_description: "jwks_uri must use HTTPS"
1287
+ });
1288
+ if (isPrivateHostname(uri.hostname)) throw new APIError("BAD_REQUEST", {
1289
+ error: "invalid_client_metadata",
1290
+ error_description: "jwks_uri must not point to a private or reserved address"
1291
+ });
1292
+ if (settings?.ctx && !settings.ctx.context.isTrustedOrigin(uri.href)) throw new APIError("BAD_REQUEST", {
1293
+ error: "invalid_client_metadata",
1294
+ error_description: "jwks_uri must belong to a trusted origin"
1295
+ });
1296
+ } catch (e) {
1297
+ if (e instanceof APIError) throw e;
1298
+ throw new APIError("BAD_REQUEST", {
1299
+ error: "invalid_client_metadata",
1300
+ error_description: "jwks_uri must be a valid URL"
1301
+ });
1302
+ }
1303
+ if (client.jwks) {
1304
+ const keys = Array.isArray(client.jwks) ? client.jwks : client.jwks.keys;
1305
+ if (!Array.isArray(keys) || keys.length === 0) throw new APIError("BAD_REQUEST", {
1306
+ error: "invalid_client_metadata",
1307
+ error_description: "jwks must be a non-empty array of JWK objects or a JWKS document {keys:[...]}"
1308
+ });
1309
+ }
1310
+ } else if (client.jwks || client.jwks_uri) throw new APIError("BAD_REQUEST", {
1311
+ error: "invalid_client_metadata",
1312
+ error_description: "jwks and jwks_uri are only allowed with private_key_jwt authentication"
1313
+ });
1267
1314
  }
1268
1315
  async function createOAuthClientEndpoint(ctx, opts, settings) {
1269
1316
  const body = ctx.body;
1270
1317
  const session = await getSessionFromCtx(ctx);
1271
1318
  const isPublic = body.token_endpoint_auth_method === "none";
1272
- await checkOAuthClient(ctx.body, opts, settings);
1319
+ const isPrivateKeyJwt = body.token_endpoint_auth_method === "private_key_jwt";
1320
+ await checkOAuthClient(ctx.body, opts, {
1321
+ ...settings,
1322
+ ctx
1323
+ });
1273
1324
  const clientId = opts.generateClientId?.() || generateRandomString(32, "a-z", "A-Z");
1274
- const clientSecret = isPublic ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
1325
+ const clientSecret = isPublic || isPrivateKeyJwt ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
1275
1326
  const storedClientSecret = clientSecret ? await storeClientSecret(ctx, opts, clientSecret) : void 0;
1276
1327
  const iat = Math.floor(Date.now() / 1e3);
1277
1328
  const referenceId = opts.clientReference ? await opts.clientReference({
@@ -1281,8 +1332,6 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1281
1332
  const schema = oauthToSchema({
1282
1333
  ...body ?? {},
1283
1334
  disabled: void 0,
1284
- jwks: void 0,
1285
- jwks_uri: void 0,
1286
1335
  client_secret_expires_at: storedClientSecret ? settings.isRegister && opts?.clientRegistrationClientSecretExpiration ? toExpJWT(opts.clientRegistrationClientSecretExpiration, iat) : 0 : void 0,
1287
1336
  client_id: clientId,
1288
1337
  client_secret: storedClientSecret,
@@ -1317,7 +1366,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1317
1366
  * @returns
1318
1367
  */
1319
1368
  function oauthToSchema(input) {
1320
- 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, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1369
+ const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: inputJwks, jwks_uri: jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1321
1370
  const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
1322
1371
  const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
1323
1372
  const scopes = _scope?.split(" ");
@@ -1325,6 +1374,7 @@ function oauthToSchema(input) {
1325
1374
  ...rest && Object.keys(rest).length ? rest : {},
1326
1375
  ...inputMetadata && typeof inputMetadata === "object" ? inputMetadata : {}
1327
1376
  };
1377
+ const metadata = Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0;
1328
1378
  return {
1329
1379
  clientId,
1330
1380
  clientSecret,
@@ -1347,6 +1397,8 @@ function oauthToSchema(input) {
1347
1397
  tokenEndpointAuthMethod,
1348
1398
  grantTypes,
1349
1399
  responseTypes,
1400
+ jwks: inputJwks ? JSON.stringify({ keys: Array.isArray(inputJwks) ? inputJwks : inputJwks.keys }) : void 0,
1401
+ jwksUri,
1350
1402
  public: _public,
1351
1403
  type,
1352
1404
  skipConsent,
@@ -1354,7 +1406,7 @@ function oauthToSchema(input) {
1354
1406
  requirePKCE,
1355
1407
  subjectType,
1356
1408
  referenceId,
1357
- metadata: Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0
1409
+ metadata
1358
1410
  };
1359
1411
  }
1360
1412
  /**
@@ -1364,7 +1416,7 @@ function oauthToSchema(input) {
1364
1416
  * @returns
1365
1417
  */
1366
1418
  function schemaToOAuth(input) {
1367
- 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, subjectType, referenceId, metadata } = input;
1419
+ const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, subjectType, referenceId, metadata } = input;
1368
1420
  const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
1369
1421
  const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
1370
1422
  const _scopes = scopes?.join(" ");
@@ -1382,6 +1434,8 @@ function schemaToOAuth(input) {
1382
1434
  contacts: contacts ?? void 0,
1383
1435
  tos_uri: tos ?? void 0,
1384
1436
  policy_uri: policy ?? void 0,
1437
+ jwks: jwks ? JSON.parse(jwks).keys : void 0,
1438
+ jwks_uri: jwksUri ?? void 0,
1385
1439
  software_id: softwareId ?? void 0,
1386
1440
  software_version: softwareVersion ?? void 0,
1387
1441
  software_statement: softwareStatement ?? void 0,
@@ -1558,7 +1612,14 @@ async function updateClientEndpoint(ctx, opts) {
1558
1612
  await checkOAuthClient({
1559
1613
  ...schemaToOAuth(client),
1560
1614
  ...updates
1561
- }, opts);
1615
+ }, opts, { ctx });
1616
+ const schemaUpdates = { ...oauthToSchema(updates) };
1617
+ if (updates.token_endpoint_auth_method) if (updates.token_endpoint_auth_method === "private_key_jwt") schemaUpdates.clientSecret = null;
1618
+ else {
1619
+ schemaUpdates.jwks = null;
1620
+ schemaUpdates.jwksUri = null;
1621
+ if (!schemaUpdates.clientSecret) schemaUpdates.clientSecret = await storeClientSecret(ctx, opts, opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z"));
1622
+ }
1562
1623
  const updatedClient = await ctx.context.adapter.update({
1563
1624
  model: "oauthClient",
1564
1625
  where: [{
@@ -1566,7 +1627,7 @@ async function updateClientEndpoint(ctx, opts) {
1566
1627
  value: clientId
1567
1628
  }],
1568
1629
  update: {
1569
- ...oauthToSchema(updates),
1630
+ ...schemaUpdates,
1570
1631
  updatedAt: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3)
1571
1632
  }
1572
1633
  });
@@ -1604,7 +1665,7 @@ async function rotateClientSecretEndpoint(ctx, opts) {
1604
1665
  if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
1605
1666
  } else throw new APIError("UNAUTHORIZED");
1606
1667
  if (client.public || !client.clientSecret) throw new APIError("BAD_REQUEST", {
1607
- error_description: "public clients cannot be updated",
1668
+ error_description: "secret rotation is only available for clients using client_secret authentication",
1608
1669
  error: "invalid_client"
1609
1670
  });
1610
1671
  const clientSecret = opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
@@ -1649,8 +1710,11 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
1649
1710
  token_endpoint_auth_method: z.enum([
1650
1711
  "none",
1651
1712
  "client_secret_basic",
1652
- "client_secret_post"
1713
+ "client_secret_post",
1714
+ "private_key_jwt"
1653
1715
  ]).default("client_secret_basic").optional(),
1716
+ jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
1717
+ jwks_uri: z.string().optional(),
1654
1718
  grant_types: z.array(z.enum([
1655
1719
  "authorization_code",
1656
1720
  "client_credentials",
@@ -1832,8 +1896,11 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
1832
1896
  token_endpoint_auth_method: z.enum([
1833
1897
  "none",
1834
1898
  "client_secret_basic",
1835
- "client_secret_post"
1899
+ "client_secret_post",
1900
+ "private_key_jwt"
1836
1901
  ]).default("client_secret_basic").optional(),
1902
+ jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
1903
+ jwks_uri: z.string().optional(),
1837
1904
  grant_types: z.array(z.enum([
1838
1905
  "authorization_code",
1839
1906
  "client_credentials",
@@ -2391,13 +2458,8 @@ async function revokeAccessToken(ctx, opts, clientId, token) {
2391
2458
  });
2392
2459
  }
2393
2460
  async function revokeEndpoint(ctx, opts) {
2394
- let { client_id, client_secret, token, token_type_hint } = ctx.body;
2395
- const authorization = ctx.request?.headers.get("authorization") || null;
2396
- if (authorization?.startsWith("Basic ")) {
2397
- const res = basicToClientCredentials(authorization);
2398
- client_id = res?.client_id;
2399
- client_secret = res?.client_secret;
2400
- }
2461
+ let { token, token_type_hint } = ctx.body;
2462
+ const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/revoke`));
2401
2463
  if (!client_id) throw new APIError$1("UNAUTHORIZED", {
2402
2464
  error_description: "missing required credentials",
2403
2465
  error: "invalid_client"
@@ -2407,7 +2469,7 @@ async function revokeEndpoint(ctx, opts) {
2407
2469
  error_description: "missing a required token for introspection",
2408
2470
  error: "invalid_request"
2409
2471
  });
2410
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
2472
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
2411
2473
  try {
2412
2474
  if (token_type_hint === void 0 || token_type_hint === "access_token") try {
2413
2475
  return await revokeAccessToken(ctx, opts, client.clientId, token);
@@ -2542,6 +2604,14 @@ const schema = {
2542
2604
  type: "string",
2543
2605
  required: false
2544
2606
  },
2607
+ jwks: {
2608
+ type: "string",
2609
+ required: false
2610
+ },
2611
+ jwksUri: {
2612
+ type: "string",
2613
+ required: false
2614
+ },
2545
2615
  grantTypes: {
2546
2616
  type: "string[]",
2547
2617
  required: false
@@ -2806,7 +2876,7 @@ const oauthProvider = (options) => {
2806
2876
  queryParams.delete("sig");
2807
2877
  queryParams.delete("exp");
2808
2878
  await oAuthState.set({ query: queryParams.toString() });
2809
- if (ctx.path === "/sign-in/social" || ctx.path === "/sign-in/oauth2") {
2879
+ if (ctx.path === "/sign-in/social") {
2810
2880
  if (ctx.body.additionalData?.query) return;
2811
2881
  if (!ctx.body.additionalData) ctx.body.additionalData = {};
2812
2882
  ctx.body.additionalData.query = queryParams.toString();
@@ -2840,12 +2910,15 @@ const oauthProvider = (options) => {
2840
2910
  metadata: { SERVER_ONLY: true }
2841
2911
  }, async (ctx) => {
2842
2912
  if (opts.scopes && opts.scopes.includes("openid")) return oidcServerMetadata(ctx, opts);
2843
- else return authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
2844
- scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
2845
- public_client_supported: opts.allowUnauthenticatedClientRegistration,
2846
- grant_types_supported: opts.grantTypes,
2847
- jwt_disabled: opts.disableJwtPlugin
2848
- });
2913
+ else return {
2914
+ ...authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
2915
+ scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
2916
+ public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
2917
+ grant_types_supported: opts.grantTypes,
2918
+ jwt_disabled: opts.disableJwtPlugin
2919
+ }),
2920
+ ...mergeDiscoveryMetadata(opts.clientDiscovery)
2921
+ };
2849
2922
  }),
2850
2923
  getOpenIdConfig: createAuthEndpoint("/.well-known/openid-configuration", {
2851
2924
  method: "GET",
@@ -3044,6 +3117,8 @@ const oauthProvider = (options) => {
3044
3117
  ]),
3045
3118
  client_id: z.string().optional(),
3046
3119
  client_secret: z.string().optional(),
3120
+ client_assertion: z.string().optional(),
3121
+ client_assertion_type: z.string().optional(),
3047
3122
  code: z.string().optional(),
3048
3123
  code_verifier: z.string().optional(),
3049
3124
  redirect_uri: SafeUrlSchema.optional(),
@@ -3168,6 +3243,8 @@ const oauthProvider = (options) => {
3168
3243
  body: z.object({
3169
3244
  client_id: z.string().optional(),
3170
3245
  client_secret: z.string().optional(),
3246
+ client_assertion: z.string().optional(),
3247
+ client_assertion_type: z.string().optional(),
3171
3248
  token: z.string(),
3172
3249
  token_type_hint: z.enum(["access_token", "refresh_token"]).optional()
3173
3250
  }),
@@ -3286,6 +3363,8 @@ const oauthProvider = (options) => {
3286
3363
  body: z.object({
3287
3364
  client_id: z.string().optional(),
3288
3365
  client_secret: z.string().optional(),
3366
+ client_assertion: z.string().optional(),
3367
+ client_assertion_type: z.string().optional(),
3289
3368
  token: z.string(),
3290
3369
  token_type_hint: z.enum(["access_token", "refresh_token"]).optional()
3291
3370
  }),
@@ -3483,8 +3562,11 @@ const oauthProvider = (options) => {
3483
3562
  token_endpoint_auth_method: z.enum([
3484
3563
  "none",
3485
3564
  "client_secret_basic",
3486
- "client_secret_post"
3565
+ "client_secret_post",
3566
+ "private_key_jwt"
3487
3567
  ]).default("client_secret_basic").optional(),
3568
+ jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
3569
+ jwks_uri: z.string().optional(),
3488
3570
  grant_types: z.array(z.enum([
3489
3571
  "authorization_code",
3490
3572
  "client_credentials",
@@ -3589,7 +3671,8 @@ const oauthProvider = (options) => {
3589
3671
  enum: [
3590
3672
  "none",
3591
3673
  "client_secret_basic",
3592
- "client_secret_post"
3674
+ "client_secret_post",
3675
+ "private_key_jwt"
3593
3676
  ]
3594
3677
  },
3595
3678
  grant_types: {
@@ -3974,10 +4057,22 @@ function authServerMetadata(ctx, opts, overrides) {
3974
4057
  token_endpoint_auth_methods_supported: [
3975
4058
  ...overrides?.public_client_supported ? ["none"] : [],
3976
4059
  "client_secret_basic",
3977
- "client_secret_post"
4060
+ "client_secret_post",
4061
+ "private_key_jwt"
3978
4062
  ],
3979
- introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
3980
- revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
4063
+ token_endpoint_auth_signing_alg_values_supported: [...ASSERTION_SIGNING_ALGORITHMS],
4064
+ introspection_endpoint_auth_methods_supported: [
4065
+ "client_secret_basic",
4066
+ "client_secret_post",
4067
+ "private_key_jwt"
4068
+ ],
4069
+ introspection_endpoint_auth_signing_alg_values_supported: [...ASSERTION_SIGNING_ALGORITHMS],
4070
+ revocation_endpoint_auth_methods_supported: [
4071
+ "client_secret_basic",
4072
+ "client_secret_post",
4073
+ "private_key_jwt"
4074
+ ],
4075
+ revocation_endpoint_auth_signing_alg_values_supported: [...ASSERTION_SIGNING_ALGORITHMS],
3981
4076
  code_challenge_methods_supported: ["S256"],
3982
4077
  authorization_response_iss_parameter_supported: true
3983
4078
  };
@@ -3988,7 +4083,7 @@ function oidcServerMetadata(ctx, opts) {
3988
4083
  return {
3989
4084
  ...authServerMetadata(ctx, jwtPluginOptions, {
3990
4085
  scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
3991
- public_client_supported: opts.allowUnauthenticatedClientRegistration,
4086
+ public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
3992
4087
  grant_types_supported: opts.grantTypes,
3993
4088
  jwt_disabled: opts.disableJwtPlugin
3994
4089
  }),
@@ -4004,7 +4099,8 @@ function oidcServerMetadata(ctx, opts) {
4004
4099
  "create",
4005
4100
  "select_account",
4006
4101
  "none"
4007
- ]
4102
+ ],
4103
+ ...mergeDiscoveryMetadata(opts.clientDiscovery)
4008
4104
  };
4009
4105
  }
4010
4106
  const METADATA_CACHE_CONTROL = "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400";
@@ -4050,4 +4146,4 @@ const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
4050
4146
  };
4051
4147
  };
4052
4148
  //#endregion
4053
- export { authServerMetadata, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
4149
+ export { authServerMetadata, checkOAuthClient, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata };
@@ -0,0 +1,56 @@
1
+ import { isAPIError } from "better-auth/api";
2
+ import { verifyAccessToken } from "better-auth/oauth2";
3
+ import { APIError as APIError$1 } from "better-call";
4
+ //#region src/mcp.ts
5
+ /**
6
+ * A request middleware handler that checks and responds with
7
+ * a WWW-Authenticate header for unauthenticated responses.
8
+ *
9
+ * @external
10
+ */
11
+ const mcpHandler = (verifyOptions, handler, opts) => {
12
+ return async (req) => {
13
+ const authorization = req.headers?.get("authorization") ?? void 0;
14
+ const accessToken = authorization?.startsWith("Bearer ") ? authorization.replace("Bearer ", "") : authorization;
15
+ try {
16
+ if (!accessToken?.length) throw new APIError$1("UNAUTHORIZED", { message: "missing authorization header" });
17
+ return handler(req, await verifyAccessToken(accessToken, verifyOptions));
18
+ } catch (error) {
19
+ try {
20
+ handleMcpErrors(error, verifyOptions.verifyOptions.audience, opts);
21
+ } catch (err) {
22
+ if (err instanceof APIError$1) return new Response(err.message, {
23
+ ...err,
24
+ status: err.statusCode
25
+ });
26
+ throw new Error(String(err));
27
+ }
28
+ throw new Error(String(error));
29
+ }
30
+ };
31
+ };
32
+ /**
33
+ * The following handles all MCP errors and API errors
34
+ *
35
+ * @internal
36
+ */
37
+ function handleMcpErrors(error, resource, opts) {
38
+ if (isAPIError(error) && error.status === "UNAUTHORIZED") {
39
+ const wwwAuthenticateValue = (Array.isArray(resource) ? resource : [resource]).map((v) => {
40
+ let audiencePath;
41
+ if (URL.canParse?.(v)) {
42
+ const url = new URL(v);
43
+ audiencePath = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
44
+ return `Bearer resource_metadata="${url.origin}/.well-known/oauth-protected-resource${audiencePath}"`;
45
+ } else {
46
+ const resourceMetadata = opts?.resourceMetadataMappings?.[v];
47
+ if (!resourceMetadata) throw new APIError$1("INTERNAL_SERVER_ERROR", { message: `missing resource_metadata mapping for ${v}` });
48
+ return `Bearer resource_metadata=${resourceMetadata}`;
49
+ }
50
+ }).join(", ");
51
+ throw new APIError$1("UNAUTHORIZED", { message: error.message }, { "WWW-Authenticate": wwwAuthenticateValue });
52
+ } else if (error instanceof Error) throw error;
53
+ else throw new Error(error);
54
+ }
55
+ //#endregion
56
+ export { mcpHandler as n, handleMcpErrors as t };