@better-auth/oauth-provider 1.7.9 → 1.7.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
- import { A as claimsRequestParameterSchema, C as clientJwksSchema, D as STANDARD_CLAIM_NAMES, E as STANDARD_CLAIMS, M as getRequestedUserInfoClaims, N as isValidOidcClaimsRequest, O as getSupportedClaims, S as authorizationQuerySchema, _ as logStartupSeedFailure, a as invalidateRefreshFamily, b as ResourceUriSchema, c as normalizeClientCredentialsScopes, d as assertIdentifierValid, f as extractRepeatedResourceFromForm, g as logEnforcePerClientResourcesResolution, h as isAudienceClaimAllowed, j as filterClaimsRequestUserInfoClaims, k as canSatisfyEssentialAcrRequest, l as validateClientCredentialsScopes, m as invalidateResourceCache, o as tokenEndpoint, p as getResource, r as decodeRefreshToken, s as userInfoEndpoint, t as introspectEndpoint, u as JWS_ALGORITHMS, v as resolveResourcePolicy, w as clientRegistrationRequestSchema, x as SafeUrlSchema, y as seedResourcesOnce } from "./introspect-7WCMPe5Z.mjs";
3
- import { B as validateOAuthProviderExtensions, C as storeToken, E as validateClientCredentials, I as getSupportedAuthMethods, L as getSupportedGrantTypes, O as verifyOAuthQueryParams, P as getClientDiscoveries, S as storeClientSecret, T as toResourceList, _ as removeMaxAgeFromQuery, a as getClient, b as resolveSubjectIdentifier, c as getStoredToken, d as mergeDiscoveryMetadata, g as parsePrompt, h as parseClientMetadata, i as extractClientCredentials, k as applyOAuthProviderMetadataExtensions, l as isPKCERequired, m as parseBearerToken, n as decryptStoredClientSecret, o as getJwtPlugin, r as destructureCredentials, t as clientAllowsGrant, u as isSessionFreshForSignedQuery, v as removePromptFromQuery, x as searchParamsToQuery, z as isExtensionTokenEndpointAuthMethod } from "./utils-DKU5wblX.mjs";
2
+ import { A as canSatisfyEssentialAcrRequest, C as authorizationQuerySchema, D as STANDARD_CLAIMS, M as filterClaimsRequestUserInfoClaims, N as getRequestedUserInfoClaims, O as STANDARD_CLAIM_NAMES, P as isValidOidcClaimsRequest, S as SafeUrlSchema, T as clientRegistrationRequestSchema, _ as logEnforcePerClientResourcesResolution, a as invalidateRefreshFamily, b as seedResourcesOnce, c as normalizeClientCredentialsScopes, d as JWS_ALGORITHMS, f as assertIdentifierValid, g as isAudienceClaimAllowed, h as invalidateResourceCache, j as claimsRequestParameterSchema, k as getSupportedClaims, l as validateClientCredentialsScopes, m as getResource, o as tokenEndpoint, p as extractRepeatedResourceFromForm, r as decodeRefreshToken, s as userInfoEndpoint, t as introspectEndpoint, u as getEndpointUrl, v as logStartupSeedFailure, w as clientJwksSchema, x as ResourceUriSchema, y as resolveResourcePolicy } from "./introspect-B-5vOvYK.mjs";
3
+ import { A as validateClientCredentials, B as getSupportedAuthMethods, D as storeToken, E as storeClientSecret, M as verifyOAuthQueryParams, N as applyOAuthProviderMetadataExtensions, R as getClientDiscoveries, S as removePromptFromQuery, T as searchParamsToQuery, U as isExtensionTokenEndpointAuthMethod, V as getSupportedGrantTypes, W as validateOAuthProviderExtensions, a as extractClientCredentials, b as parsePrompt, c as getConsentReferenceId, d as getStoredToken, f as isInvalidJwtError, h as mergeDiscoveryMetadata, i as destructureCredentials, k as toResourceList, l as getJwtPlugin, m as isSessionFreshForSignedQuery, n as clientAllowsGrant, o as findConsent, p as isPKCERequired, r as decryptStoredClientSecret, s as getClient, t as assertNotTrustedClient, v as parseBearerToken, w as resolveSubjectIdentifier, x as removeMaxAgeFromQuery, y as parseClientMetadata } from "./utils-vIo_wPv5.mjs";
4
4
  import { a as setSignedOAuthQueryParameterNames, i as postLoginClearedParam, n as canonicalizeOAuthQueryParams, o as signedQueryIssuedAtParam, r as getSignedQueryIssuedAt } from "./signed-query-Df1MNiSH.mjs";
5
- import { t as PACKAGE_VERSION } from "./version-DVUrz-nv.mjs";
5
+ import { t as PACKAGE_VERSION } from "./version-DtVKOwOn.mjs";
6
6
  import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
7
7
  import { isLoopbackHost, isLoopbackIP, isPublicRoutableHost } from "@better-auth/core/utils/host";
8
8
  import { appendQueryParams } from "@better-auth/core/utils/url";
@@ -17,8 +17,8 @@ import { deleteSessionCookie, parseSetCookieHeader } from "better-auth/cookies";
17
17
  import { mergeSchema } from "better-auth/db";
18
18
  import { CLIENT_ASSERTION_TYPE, DPOP_SIGNING_ALGORITHMS, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
19
19
  import { createHash } from "@better-auth/utils/hash";
20
+ import { compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader, errors, jwtVerify } from "jose";
20
21
  import { getJwks, stripAccessTokenAuthorizationScheme } from "better-auth/oauth2";
21
- import { compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify } from "jose";
22
22
  import { isNativePrivateUseRedirectUri } from "@better-auth/core/utils/redirect-uri";
23
23
  import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
24
24
  import { encodeDeterministicId } from "@better-auth/core/db/internal";
@@ -64,28 +64,12 @@ async function consentEndpoint(ctx, opts, authorize) {
64
64
  ctx.query = searchParamsToQuery(query);
65
65
  return await authorize(ctx);
66
66
  }
67
- const referenceId = await opts.postLogin?.consentReferenceId?.({
67
+ const referenceId = await getConsentReferenceId(opts, {
68
68
  user: session?.user,
69
69
  session: session?.session,
70
70
  scopes: requestedScopes ?? originalRequestedScopes
71
71
  });
72
- const foundConsent = await ctx.context.adapter.findOne({
73
- model: "oauthConsent",
74
- where: [
75
- {
76
- field: "clientId",
77
- value: clientId
78
- },
79
- {
80
- field: "userId",
81
- value: session?.user.id
82
- },
83
- ...referenceId ? [{
84
- field: "referenceId",
85
- value: referenceId
86
- }] : []
87
- ]
88
- });
72
+ const foundConsent = await findConsent(ctx, clientId, session?.user.id, referenceId);
89
73
  const iat = Math.floor(Date.now() / 1e3);
90
74
  const resource = query.getAll("resource");
91
75
  const consent = {
@@ -1032,6 +1016,17 @@ function validatePublicClientJwks(input) {
1032
1016
  jwks: { keys }
1033
1017
  };
1034
1018
  }
1019
+ /**
1020
+ * Throws `invalid_client_metadata` unless `jwks` is a public JWK Set that
1021
+ * {@link validatePublicClientJwks} accepts.
1022
+ */
1023
+ function assertValidClientJwks(jwks) {
1024
+ const result = validatePublicClientJwks(jwks);
1025
+ if (!result.valid) throw new APIError("BAD_REQUEST", {
1026
+ error: "invalid_client_metadata",
1027
+ error_description: result.error
1028
+ });
1029
+ }
1035
1030
  //#endregion
1036
1031
  //#region src/client-metadata.ts
1037
1032
  const OAUTH_CLIENT_RECORD_FIELD_NAMES = Object.freeze(Object.keys({
@@ -1492,8 +1487,20 @@ async function consumeClientAssertion(ctx, opts, params) {
1492
1487
  * Validates: signature, iss=client_id, sub=client_id, aud=the endpoint serving
1493
1488
  * the assertion or the normalized OP issuer, exp, assertion max lifetime,
1494
1489
  * jti uniqueness (replay prevention).
1490
+ *
1491
+ * Returns the client record the signature was checked against, so a caller that
1492
+ * mutates the client can guard its write on exactly that snapshot.
1493
+ *
1494
+ * With `endpointAudienceOnly`, the issuer is not an accepted audience, so only
1495
+ * an assertion whose `aud` includes `expectedAudience` verifies. Endpoints that
1496
+ * change client credentials use it, so an assertion minted for the token
1497
+ * endpoint or the issuer cannot be redirected to them.
1498
+ *
1499
+ * `assertClient` runs on the looked-up client before any key is fetched or the
1500
+ * jti is consumed, so a caller can refuse a client it does not serve without
1501
+ * spending the assertion or reaching out to a `jwks_uri`.
1495
1502
  */
1496
- async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertionType, clientIdHint, expectedAudience) {
1503
+ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertionType, clientIdHint, expectedAudience, options) {
1497
1504
  if (clientAssertionType !== CLIENT_ASSERTION_TYPE) throw new APIError$1("BAD_REQUEST", {
1498
1505
  error_description: "unsupported client_assertion_type",
1499
1506
  error: "invalid_client"
@@ -1531,7 +1538,7 @@ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertion
1531
1538
  });
1532
1539
  const client = await getClient(ctx, opts, clientId);
1533
1540
  if (!client) throw new APIError$1("BAD_REQUEST", {
1534
- error_description: "unknown client",
1541
+ error_description: "client not found",
1535
1542
  error: "invalid_client"
1536
1543
  });
1537
1544
  if (client.disabled) throw new APIError$1("BAD_REQUEST", {
@@ -1542,10 +1549,11 @@ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertion
1542
1549
  error_description: "client is not registered for private_key_jwt authentication",
1543
1550
  error: "invalid_client"
1544
1551
  });
1552
+ options?.assertClient?.(client);
1545
1553
  const jwks = await fetchClientJwks(ctx, opts, client);
1546
1554
  const endpointAudience = expectedAudience ?? `${ctx.context.baseURL}/oauth2/token`;
1547
1555
  const issuerAudience = getIssuer(ctx, opts);
1548
- const acceptedAudiences = [...new Set([endpointAudience, issuerAudience])];
1556
+ const acceptedAudiences = options?.endpointAudienceOnly ? [endpointAudience] : [...new Set([endpointAudience, issuerAudience])];
1549
1557
  const verifyOpts = {
1550
1558
  issuer: clientId,
1551
1559
  subject: clientId,
@@ -1581,7 +1589,10 @@ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertion
1581
1589
  payload,
1582
1590
  expectedAudience: acceptedAudience
1583
1591
  });
1584
- return { clientId };
1592
+ return {
1593
+ clientId,
1594
+ client
1595
+ };
1585
1596
  }
1586
1597
  //#endregion
1587
1598
  //#region src/utils/initial-access-token.ts
@@ -1659,6 +1670,21 @@ function resolveRegistrationResponseTypes(client, grantTypes) {
1659
1670
  if (client.response_types) return client.response_types;
1660
1671
  return grantTypes.includes("authorization_code") ? ["code"] : void 0;
1661
1672
  }
1673
+ /**
1674
+ * The requested grants this server supports. Clients such as MCP connectors
1675
+ * send one grant union to every server, so a metadata document or a dynamic
1676
+ * registration may list grants this server does not issue. Throws when the
1677
+ * request shares no grant with the server.
1678
+ */
1679
+ function resolveMutuallySupportedGrantTypes(opts, grantTypes) {
1680
+ const supportedGrantTypes = new Set(getSupportedGrantTypes(opts));
1681
+ const mutualGrantTypes = grantTypes.filter((grantType) => supportedGrantTypes.has(grantType));
1682
+ if (mutualGrantTypes.length === 0) throw new APIError("BAD_REQUEST", {
1683
+ error: "invalid_client_metadata",
1684
+ error_description: `no mutually supported grant_type among ${grantTypes.join(", ")}`
1685
+ });
1686
+ return mutualGrantTypes;
1687
+ }
1662
1688
  function applyOAuthClientRegistrationDefaults(client, defaultApplicationType = "web") {
1663
1689
  const grantTypes = resolveRegistrationGrantTypes(client);
1664
1690
  return {
@@ -1684,15 +1710,15 @@ function isNonHttpRedirectUri(redirectUri) {
1684
1710
  /**
1685
1711
  * Server-owned default for dynamic registrations that omit `application_type`.
1686
1712
  * `"infer"` classifies as native when any redirect URI uses a non-http(s)
1687
- * scheme, because a web client can never register one. Every URI is then
1688
- * validated against the native rules. A sent `application_type` is never
1689
- * overridden because `applyOAuthClientRegistrationDefaults` only fills the
1690
- * absent field.
1713
+ * scheme or http on an exact loopback host, because a web client can never
1714
+ * register either. Every URI is then validated against the native rules. A
1715
+ * sent `application_type` is never overridden because
1716
+ * `applyOAuthClientRegistrationDefaults` only fills the absent field.
1691
1717
  */
1692
1718
  function resolveDynamicRegistrationDefaultApplicationType(opts, client) {
1693
1719
  const configured = opts.clientRegistrationDefaultApplicationType ?? "web";
1694
1720
  if (configured !== "infer") return configured;
1695
- return (client.redirect_uris ?? []).some(isNonHttpRedirectUri) ? "native" : "web";
1721
+ return (client.redirect_uris ?? []).some((uri) => isNonHttpRedirectUri(uri) || isExactLoopbackHttpRedirectUri(uri)) ? "native" : "web";
1696
1722
  }
1697
1723
  function invalidRedirectUri(description) {
1698
1724
  throw new APIError("BAD_REQUEST", {
@@ -1710,6 +1736,10 @@ function getRawHttpHostname(redirectUri) {
1710
1736
  }
1711
1737
  return (hostAndPort.split(":")[0] ?? "").toLowerCase();
1712
1738
  }
1739
+ function isExactLoopbackHttpRedirectUri(redirectUri) {
1740
+ const hostname = getRawHttpHostname(redirectUri);
1741
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
1742
+ }
1713
1743
  function assertDynamicRegistrationResourcesAllowed(opts, requestedResources) {
1714
1744
  const allowedResources = new Set([...opts.clientRegistrationDefaultResources ?? [], ...opts.clientRegistrationAllowedResources ?? []]);
1715
1745
  for (const identifier of requestedResources) if (!allowedResources.has(identifier)) throw new APIError("BAD_REQUEST", {
@@ -1745,8 +1775,6 @@ function validateClientRedirectUri(redirectUri, applicationType) {
1745
1775
  const isHttps = url.protocol === "https:";
1746
1776
  if (/^localhost\.+$/i.test(url.hostname)) invalidRedirectUri(`redirect URI localhost must not include trailing dots: ${redirectUri}`);
1747
1777
  const isRedirectLoopback = isLoopbackIP(url.hostname) || url.hostname === "localhost";
1748
- const rawHttpHostname = getRawHttpHostname(redirectUri);
1749
- const isAllowedNativeHttpLoopback = rawHttpHostname === "localhost" || rawHttpHostname === "127.0.0.1" || rawHttpHostname === "[::1]";
1750
1778
  if (applicationType === "web") {
1751
1779
  if (!isHttps || isRedirectLoopback) invalidRedirectUri(`web clients require https redirect URIs on non-loopback hosts: ${redirectUri}`);
1752
1780
  return;
@@ -1756,18 +1784,11 @@ function validateClientRedirectUri(redirectUri, applicationType) {
1756
1784
  return;
1757
1785
  }
1758
1786
  if (isHttp) {
1759
- if (!isAllowedNativeHttpLoopback) invalidRedirectUri(`native clients may use http only on the exact loopback hosts localhost, 127.0.0.1, or [::1]: ${redirectUri}`);
1787
+ if (!isExactLoopbackHttpRedirectUri(redirectUri)) invalidRedirectUri(`native clients may use http only on the exact loopback hosts localhost, 127.0.0.1, or [::1]: ${redirectUri}`);
1760
1788
  return;
1761
1789
  }
1762
1790
  if (!isNativePrivateUseRedirectUri(url)) invalidRedirectUri(`native private-use redirect URI schemes must not use a reserved scheme; they must be an authority-free reverse-domain URI or a custom-scheme URI with a host and a path: ${redirectUri}`);
1763
1791
  }
1764
- function assertValidRegistrationJwks(jwks) {
1765
- const result = validatePublicClientJwks(jwks);
1766
- if (!result.valid) throw new APIError("BAD_REQUEST", {
1767
- error: "invalid_client_metadata",
1768
- error_description: result.error
1769
- });
1770
- }
1771
1792
  async function registerEndpoint(ctx, opts) {
1772
1793
  const body = ctx.body;
1773
1794
  if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
@@ -1781,8 +1802,15 @@ async function registerEndpoint(ctx, opts) {
1781
1802
  "WWW-Authenticate": "Bearer",
1782
1803
  ...NO_STORE_HEADERS
1783
1804
  });
1805
+ const grantTypes = body.grant_types?.length ? resolveMutuallySupportedGrantTypes(opts, body.grant_types) : body.grant_types;
1806
+ const droppedAuthorizationCode = body.grant_types?.includes("authorization_code") && !grantTypes?.includes("authorization_code");
1807
+ const metadata = {
1808
+ ...body,
1809
+ grant_types: grantTypes,
1810
+ response_types: droppedAuthorizationCode ? void 0 : body.response_types
1811
+ };
1784
1812
  if (!session && !isTokenAuthorized) {
1785
- if (body.grant_types?.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
1813
+ if (metadata.grant_types?.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
1786
1814
  error: "invalid_client_metadata",
1787
1815
  error_description: "client_credentials grant requires authenticated registration"
1788
1816
  });
@@ -1794,7 +1822,7 @@ async function registerEndpoint(ctx, opts) {
1794
1822
  session: session.session
1795
1823
  }) : void 0);
1796
1824
  const response = await createOAuthClientRegistration(ctx, opts, {
1797
- metadata: body,
1825
+ metadata,
1798
1826
  registrationSource: "dynamic",
1799
1827
  userId: referenceId ? void 0 : session?.session.userId,
1800
1828
  referenceId,
@@ -1827,16 +1855,14 @@ async function checkOAuthClient(client, opts, settings) {
1827
1855
  error_description: "Redirect URIs are required for authorization_code and implicit grant types"
1828
1856
  });
1829
1857
  for (const uri of clientWithDefaults.redirect_uris ?? []) validateClientRedirectUri(uri, applicationType ?? (isClientMetadataDocument ? "native" : "web"));
1830
- const supportedGrantTypes = new Set(getSupportedGrantTypes(opts));
1831
- if (isClientMetadataDocument) {
1832
- if (!grantTypes.some((grantType) => supportedGrantTypes.has(grantType))) throw new APIError("BAD_REQUEST", {
1858
+ if (isClientMetadataDocument) resolveMutuallySupportedGrantTypes(opts, grantTypes);
1859
+ else {
1860
+ const supportedGrantTypes = new Set(getSupportedGrantTypes(opts));
1861
+ for (const grantType of grantTypes) if (!supportedGrantTypes.has(grantType)) throw new APIError("BAD_REQUEST", {
1833
1862
  error: "invalid_client_metadata",
1834
- error_description: `no mutually supported grant_type among ${grantTypes.join(", ")}`
1863
+ error_description: `unsupported grant_type ${grantType}`
1835
1864
  });
1836
- } else for (const grantType of grantTypes) if (!supportedGrantTypes.has(grantType)) throw new APIError("BAD_REQUEST", {
1837
- error: "invalid_client_metadata",
1838
- error_description: `unsupported grant_type ${grantType}`
1839
- });
1865
+ }
1840
1866
  if (grantTypes.includes("authorization_code") && !responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
1841
1867
  error: "invalid_client_metadata",
1842
1868
  error_description: "When 'authorization_code' grant type is used, 'code' response type must be included"
@@ -1916,7 +1942,7 @@ async function checkOAuthClient(client, opts, settings) {
1916
1942
  error_description: "jwks_uri must be a valid URL"
1917
1943
  });
1918
1944
  }
1919
- if (clientWithDefaults.jwks) assertValidRegistrationJwks(clientWithDefaults.jwks);
1945
+ if (clientWithDefaults.jwks) assertValidClientJwks(clientWithDefaults.jwks);
1920
1946
  }
1921
1947
  if (tokenEndpointAuthMethod === "private_key_jwt" && !clientWithDefaults.jwks && !clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
1922
1948
  error: "invalid_client_metadata",
@@ -2007,7 +2033,7 @@ async function persistOAuthClientRegistration(ctx, opts, input) {
2007
2033
  user_id: owner?.referenceId ? void 0 : owner?.userId,
2008
2034
  reference_id: owner?.referenceId
2009
2035
  });
2010
- schema.clientCredentialsScopes = input.registrationSource === "clientMetadataDocument" ? input.existingClient?.clientCredentialsScopes ?? [] : input.registrationSource === "managed" ? input.clientCredentialsScopes ?? [] : [];
2036
+ schema.clientCredentialsScopes = input.registrationSource === "managed" ? input.clientCredentialsScopes ?? [] : [];
2011
2037
  schema.clientDiscoveryId = input.registrationSource === "clientMetadataDocument" ? input.clientDiscoveryId : null;
2012
2038
  if (input.registrationSource === "clientMetadataDocument" && body.application_type === void 0) schema.applicationType = null;
2013
2039
  const requestedResources = input.registrationSource === "clientMetadataDocument" ? [] : input.requestedResources ?? [];
@@ -2022,6 +2048,7 @@ async function persistOAuthClientRegistration(ctx, opts, input) {
2022
2048
  let storedClient;
2023
2049
  let created = false;
2024
2050
  if (existingClient) {
2051
+ const { clientCredentialsScopes: _ceiling, disabled: _disabled, skipConsent: _skipConsent, enableEndSession: _enableEndSession, ...refreshed } = schema;
2025
2052
  const updatedClient = await adapter.update({
2026
2053
  model: clientModel,
2027
2054
  where: [{
@@ -2032,10 +2059,7 @@ async function persistOAuthClientRegistration(ctx, opts, input) {
2032
2059
  value: clientDiscoveryId
2033
2060
  }],
2034
2061
  update: {
2035
- ...schema,
2036
- disabled: existingClient.disabled,
2037
- skipConsent: existingClient.skipConsent,
2038
- enableEndSession: existingClient.enableEndSession,
2062
+ ...refreshed,
2039
2063
  createdAt: existingClient.createdAt,
2040
2064
  updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
2041
2065
  }
@@ -2421,10 +2445,7 @@ async function deleteClientEndpoint(ctx, opts) {
2421
2445
  await assertClientPrivileges(ctx, session, opts, "delete", ctx.body.client_id);
2422
2446
  if (!session) throw new APIError("UNAUTHORIZED");
2423
2447
  const clientId = ctx.body.client_id;
2424
- if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
2425
- error_description: "trusted clients must be updated manually",
2426
- error: "invalid_client"
2427
- });
2448
+ assertNotTrustedClient(opts, clientId);
2428
2449
  const client = await getClient(ctx, opts, clientId);
2429
2450
  if (!client) throw new APIError("NOT_FOUND", {
2430
2451
  error_description: "client not found",
@@ -2448,10 +2469,7 @@ async function updateClientEndpoint(ctx, opts, settings) {
2448
2469
  await assertClientPrivileges(ctx, session, opts, "update", ctx.body.client_id);
2449
2470
  if (!session) throw new APIError("UNAUTHORIZED");
2450
2471
  const clientId = ctx.body.client_id;
2451
- if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
2452
- error_description: "trusted clients must be updated manually",
2453
- error: "invalid_client"
2454
- });
2472
+ assertNotTrustedClient(opts, clientId);
2455
2473
  const client = await getClient(ctx, opts, clientId);
2456
2474
  if (!client) throw new APIError("NOT_FOUND", {
2457
2475
  error_description: "client not found",
@@ -2519,10 +2537,7 @@ async function rotateClientSecretEndpoint(ctx, opts) {
2519
2537
  await assertClientPrivileges(ctx, session, opts, "rotate", ctx.body.client_id);
2520
2538
  if (!session) throw new APIError("UNAUTHORIZED");
2521
2539
  const clientId = ctx.body.client_id;
2522
- if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
2523
- error_description: "trusted clients must be updated manually",
2524
- error: "invalid_client"
2525
- });
2540
+ assertNotTrustedClient(opts, clientId);
2526
2541
  const client = await getClient(ctx, opts, clientId);
2527
2542
  if (!client) throw new APIError("NOT_FOUND", {
2528
2543
  error_description: "client not found",
@@ -2559,6 +2574,57 @@ async function rotateClientSecretEndpoint(ctx, opts) {
2559
2574
  clientSecret: (opts.prefix?.clientSecret ?? "") + clientSecret
2560
2575
  });
2561
2576
  }
2577
+ /**
2578
+ * The write is guarded on the exact key set the assertion was verified against.
2579
+ * A concurrent rotation changes that value, so the stale request loses instead
2580
+ * of overwriting a newer key set.
2581
+ *
2582
+ * @see https://datatracker.ietf.org/doc/html/rfc7523#section-3
2583
+ */
2584
+ async function rotateClientKeysEndpoint(ctx, opts) {
2585
+ if (!opts.allowClientKeyRotation) throw new APIError("FORBIDDEN", {
2586
+ error: "access_denied",
2587
+ error_description: "Client key rotation is disabled"
2588
+ });
2589
+ const { body } = ctx;
2590
+ assertValidClientJwks(body.jwks);
2591
+ const { client } = await verifyClientAssertion(ctx, opts, body.client_assertion, body.client_assertion_type, void 0, getEndpointUrl({
2592
+ context: ctx.context,
2593
+ request: void 0
2594
+ }, "/oauth2/client/rotate-keys"), {
2595
+ endpointAudienceOnly: true,
2596
+ assertClient: (candidate) => assertRotatable(opts, candidate)
2597
+ });
2598
+ if (!await ctx.context.adapter.incrementOne({
2599
+ model: "oauthClient",
2600
+ where: [{
2601
+ field: "clientId",
2602
+ value: client.clientId
2603
+ }, {
2604
+ field: "jwks",
2605
+ value: client.jwks
2606
+ }],
2607
+ increment: {},
2608
+ set: {
2609
+ jwks: JSON.stringify(body.jwks),
2610
+ updatedAt: /* @__PURE__ */ new Date()
2611
+ }
2612
+ })) throw new APIError("CONFLICT", {
2613
+ error: "invalid_request",
2614
+ error_description: "client keys changed while rotating; retry with a fresh client assertion"
2615
+ });
2616
+ return {
2617
+ client_id: client.clientId,
2618
+ jwks: body.jwks
2619
+ };
2620
+ }
2621
+ function assertRotatable(opts, client) {
2622
+ assertNotTrustedClient(opts, client.clientId);
2623
+ if (client.clientDiscoveryId || client.jwksUri || !client.jwks) throw new APIError("BAD_REQUEST", {
2624
+ error: "invalid_client",
2625
+ error_description: "client keys are not stored inline; rotate them where the client publishes them"
2626
+ });
2627
+ }
2562
2628
  //#endregion
2563
2629
  //#region src/oauthClient/index.ts
2564
2630
  const tokenEndpointAuthMethodSchema = z.string().trim().min(1);
@@ -3003,6 +3069,29 @@ const rotateClientSecret = (opts) => createAuthEndpoint("/oauth2/client/rotate-s
3003
3069
  }, async (ctx) => {
3004
3070
  return rotateClientSecretEndpoint(ctx, opts);
3005
3071
  });
3072
+ /**
3073
+ * Replaces a `private_key_jwt` client's own signing keys. The client proves it
3074
+ * holds a current key with a client assertion, so no session is needed.
3075
+ */
3076
+ const rotateClientKeys = (opts) => createOAuthEndpoint("/oauth2/client/rotate-keys", {
3077
+ method: "POST",
3078
+ body: z.object({
3079
+ client_assertion: z.string(),
3080
+ client_assertion_type: z.string(),
3081
+ jwks: clientJwksSchema
3082
+ }),
3083
+ errorCodesByField: {
3084
+ client_assertion: "invalid_client",
3085
+ client_assertion_type: "invalid_client",
3086
+ jwks: "invalid_client_metadata"
3087
+ },
3088
+ metadata: {
3089
+ noStore: true,
3090
+ openapi: { description: "Replaces the signing keys of a private_key_jwt client. The request is authenticated by a client assertion signed with one of the client's current keys and addressed to this endpoint. Keys left out of the new set stop verifying at once." }
3091
+ }
3092
+ }, async (ctx) => {
3093
+ return rotateClientKeysEndpoint(ctx, opts);
3094
+ });
3006
3095
  const deleteOAuthClient = (opts) => createAuthEndpoint("/oauth2/delete-client", {
3007
3096
  method: "POST",
3008
3097
  use: [sessionMiddleware],
@@ -3468,29 +3557,30 @@ const adminUnlinkClientResource = (opts) => createAuthEndpoint("/admin/oauth2/re
3468
3557
  * introspection and userinfo.
3469
3558
  */
3470
3559
  async function revokeJwtAccessToken(ctx, opts, token) {
3471
- const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
3472
- const jwtPluginOptions = jwtPlugin?.options;
3560
+ if (opts.disableJwtPlugin) throw new APIError$1("BAD_REQUEST", {
3561
+ error_description: "JWT access tokens are disabled",
3562
+ error: "invalid_request"
3563
+ });
3564
+ const jwtPlugin = getJwtPlugin(ctx.context);
3565
+ const jwtPluginOptions = jwtPlugin.options;
3566
+ let jwtPayload;
3473
3567
  try {
3474
- const verified = await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
3568
+ jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
3475
3569
  jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
3476
- return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
3570
+ return (await jwtPlugin.endpoints.getJwks(ctx))?.response;
3477
3571
  },
3478
3572
  jwksCacheKey: jwtPlugin
3479
- })), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL });
3480
- const userInfoAudience = `${ctx.context.baseURL}/oauth2/userinfo`;
3481
- if (!verified.payload.azp || !await isAudienceClaimAllowed(ctx, opts, verified.payload.aud, [userInfoAudience])) return null;
3573
+ })), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL })).payload;
3482
3574
  } catch (error) {
3483
- if (error instanceof Error) {
3484
- if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
3485
- error_description: "invalid JWT signature",
3486
- error: "invalid_request"
3487
- });
3488
- else if (error.name === "JWTExpired") return null;
3489
- else if (error.name === "JWTInvalid") return null;
3490
- throw error;
3491
- }
3492
- throw new Error(error);
3575
+ if (error instanceof errors.JWTExpired || error instanceof errors.JWTInvalid) return null;
3576
+ if (error instanceof TypeError || isInvalidJwtError(error)) throw new APIError$1("BAD_REQUEST", {
3577
+ error_description: "invalid JWT signature",
3578
+ error: "invalid_request"
3579
+ });
3580
+ throw error;
3493
3581
  }
3582
+ const userInfoAudience = `${ctx.context.baseURL}/oauth2/userinfo`;
3583
+ if (!jwtPayload.azp || !await isAudienceClaimAllowed(ctx, opts, jwtPayload.aud, [userInfoAudience])) return null;
3494
3584
  throw new APIError$1("BAD_REQUEST", {
3495
3585
  error_description: "JWT access tokens are self-contained and cannot be revoked server-side",
3496
3586
  error: "unsupported_token_type"
@@ -4443,6 +4533,10 @@ const oauthProvider = (options) => {
4443
4533
  onRequest: handleIssuerMetadataRequest,
4444
4534
  init: async (ctx) => {
4445
4535
  if (ctx.options.secondaryStorage && ctx.options.session?.storeSessionInDatabase !== true) throw new BetterAuthError("OAuth Provider requires `session.storeSessionInDatabase: true` when using secondaryStorage");
4536
+ if (opts.allowClientKeyRotation) {
4537
+ const gap = ctx.adapter.id === "memory" ? "the memory adapter" : ctx.options.advanced?.database?.generateId === "serial" ? "`advanced.database.generateId: \"serial\"`" : void 0;
4538
+ if (gap) throw new BetterAuthError(`\`allowClientKeyRotation\` requires single-use client assertions, which ${gap} does not enforce`);
4539
+ }
4446
4540
  await seedResourcesOnce(ctx, opts).catch((error) => logStartupSeedFailure(ctx, error));
4447
4541
  logEnforcePerClientResourcesResolution(opts);
4448
4542
  if (!opts.disableJwtPlugin) {
@@ -4579,12 +4673,19 @@ const oauthProvider = (options) => {
4579
4673
  description: "Consent processed successfully",
4580
4674
  content: { "application/json": { schema: {
4581
4675
  type: "object",
4582
- properties: { redirect_uri: {
4583
- type: "string",
4584
- format: "uri",
4585
- description: "The URI to redirect to, either with an authorization code or an error"
4586
- } },
4587
- required: ["redirect_uri"]
4676
+ properties: {
4677
+ redirect: {
4678
+ type: "boolean",
4679
+ description: "Always true. Indicates the caller should redirect to `url`.",
4680
+ enum: [true]
4681
+ },
4682
+ url: {
4683
+ type: "string",
4684
+ format: "uri-reference",
4685
+ description: "The absolute or relative URI to redirect to next."
4686
+ }
4687
+ },
4688
+ required: ["redirect", "url"]
4588
4689
  } } }
4589
4690
  } }
4590
4691
  } }
@@ -4603,15 +4704,22 @@ const oauthProvider = (options) => {
4603
4704
  metadata: { openapi: {
4604
4705
  description: "Continues OAuth2 authorization flow",
4605
4706
  responses: { "200": {
4606
- description: "Consent processed successfully",
4707
+ description: "Authorization flow continued successfully",
4607
4708
  content: { "application/json": { schema: {
4608
4709
  type: "object",
4609
- properties: { redirect_uri: {
4610
- type: "string",
4611
- format: "uri",
4612
- description: "The URI to redirect to, either with an authorization code or an error"
4613
- } },
4614
- required: ["redirect_uri"]
4710
+ properties: {
4711
+ redirect: {
4712
+ type: "boolean",
4713
+ description: "Always true. Indicates the caller should redirect to `url`.",
4714
+ enum: [true]
4715
+ },
4716
+ url: {
4717
+ type: "string",
4718
+ format: "uri-reference",
4719
+ description: "The absolute or relative URI to redirect to next."
4720
+ }
4721
+ },
4722
+ required: ["redirect", "url"]
4615
4723
  } } }
4616
4724
  } }
4617
4725
  } }
@@ -5257,6 +5365,7 @@ const oauthProvider = (options) => {
5257
5365
  adminUpdateOAuthClient: adminUpdateOAuthClient(opts),
5258
5366
  updateOAuthClient: updateOAuthClient(opts),
5259
5367
  rotateClientSecret: rotateClientSecret(opts),
5368
+ rotateClientKeys: rotateClientKeys(opts),
5260
5369
  deleteOAuthClient: deleteOAuthClient(opts),
5261
5370
  getOAuthConsent: getOAuthConsent(opts),
5262
5371
  getOAuthConsents: getOAuthConsents(opts),
@@ -5297,6 +5406,11 @@ const oauthProvider = (options) => {
5297
5406
  window: opts.rateLimit?.register?.window ?? 60,
5298
5407
  max: opts.rateLimit?.register?.max ?? 5
5299
5408
  }] : [],
5409
+ ...opts.rateLimit?.rotateClientKeys !== false ? [{
5410
+ pathMatcher: (path) => path === "/oauth2/client/rotate-keys",
5411
+ window: opts.rateLimit?.rotateClientKeys?.window ?? 60,
5412
+ max: opts.rateLimit?.rotateClientKeys?.max ?? 5
5413
+ }] : [],
5300
5414
  ...opts.rateLimit?.userinfo !== false ? [{
5301
5415
  pathMatcher: (path) => path === "/oauth2/userinfo",
5302
5416
  window: opts.rateLimit?.userinfo?.window ?? 60,
@@ -5606,7 +5720,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
5606
5720
  if (promptSet?.has("select_account") && !opts.selectAccount?.page) return handleRedirect(ctx, getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
5607
5721
  if (!(query.response_type === "code")) return handleRedirect(ctx, getErrorURL(ctx, "unsupported_response_type", "unsupported response type"));
5608
5722
  const client = await getClient(ctx, opts, query.client_id);
5609
- if (!client) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "client_id is required"));
5723
+ if (!client) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "client not found"));
5610
5724
  if (client.disabled) return handleRedirect(ctx, getErrorURL(ctx, "client_disabled", "client is disabled"));
5611
5725
  if (!clientAllowsGrant(client, "authorization_code")) return handleRedirect(ctx, getErrorURL(ctx, "unauthorized_client", "client is not authorized to use the authorization_code grant"));
5612
5726
  const registeredUris = client.redirectUris ?? [];
@@ -5712,7 +5826,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
5712
5826
  }
5713
5827
  }
5714
5828
  if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
5715
- const referenceId = await opts.postLogin?.consentReferenceId?.({
5829
+ const referenceId = await getConsentReferenceId(opts, {
5716
5830
  user: session.user,
5717
5831
  session: session.session,
5718
5832
  scopes: requestedScopes
@@ -5726,23 +5840,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
5726
5840
  referenceId,
5727
5841
  resource: requestedResources
5728
5842
  });
5729
- const consent = await ctx.context.adapter.findOne({
5730
- model: "oauthConsent",
5731
- where: [
5732
- {
5733
- field: "clientId",
5734
- value: client.clientId
5735
- },
5736
- {
5737
- field: "userId",
5738
- value: session.user.id
5739
- },
5740
- ...referenceId ? [{
5741
- field: "referenceId",
5742
- value: referenceId
5743
- }] : []
5744
- ]
5745
- });
5843
+ const consent = await findConsent(ctx, client.clientId, session.user.id, referenceId);
5746
5844
  if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val)) || !requestedUserInfoClaims.every((claim) => (consent.requestedUserInfoClaims ?? []).includes(claim))) {
5747
5845
  if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "consent_required", "End-User consent is required");
5748
5846
  return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
@@ -0,0 +1,25 @@
1
+ import { o as OAuthClient } from "./oauth-DXiqbRzK.mjs";
2
+ import { JSONWebKeySet } from "jose";
3
+
4
+ //#region src/client-jwks.d.ts
5
+ /** A client's inline public JWK Set, as registration and key rotation accept it. */
6
+ type ClientJwks = NonNullable<OAuthClient["jwks"]>;
7
+ type PublicClientJwksValidationResult = {
8
+ valid: true;
9
+ jwks: JSONWebKeySet;
10
+ } | {
11
+ valid: false;
12
+ error: "jwks must be an RFC 7517 JWK Set object with a non-empty keys array" | "jwks must contain only public asymmetric keys" | "jwks keys must be supported public JWKs with required key parameters" | "jwks key alg must be supported for private_key_jwt and compatible with its key type and signing curve";
13
+ };
14
+ /**
15
+ * Validates an OAuth client's public asymmetric JWK set.
16
+ *
17
+ * This boundary accepts only the RFC 7517 `{ keys: [...] }` representation.
18
+ * It performs no I/O and returns the validated set for downstream JOSE
19
+ * verification.
20
+ *
21
+ * @internal
22
+ */
23
+ declare function validatePublicClientJwks(input: unknown): PublicClientJwksValidationResult;
24
+ //#endregion
25
+ export { PublicClientJwksValidationResult as n, validatePublicClientJwks as r, ClientJwks as t };
@@ -1,6 +1,6 @@
1
- import { c as ResourceServerMetadata } from "./oauth-DAMtT2vg.mjs";
2
- import { ResourceRequestInput, VerifyAccessTokenRequestOptions } from "better-auth/oauth2";
1
+ import { c as ResourceServerMetadata } from "./oauth-DXiqbRzK.mjs";
3
2
  import { JWTPayload, JWTVerifyOptions } from "jose";
3
+ import { ResourceRequestInput, VerifyAccessTokenRequestOptions } from "better-auth/oauth2";
4
4
  import { BetterAuthOptions } from "better-auth/types";
5
5
 
6
6
  //#region src/client-resource.d.ts
@@ -1,5 +1,5 @@
1
- import { o as getJwtPlugin, s as getOAuthProviderPlugin } from "./utils-DKU5wblX.mjs";
2
- import { t as PACKAGE_VERSION } from "./version-DVUrz-nv.mjs";
1
+ import { l as getJwtPlugin, u as getOAuthProviderPlugin } from "./utils-vIo_wPv5.mjs";
2
+ import { t as PACKAGE_VERSION } from "./version-DtVKOwOn.mjs";
3
3
  import { t as createResourceServerChallenge } from "./resource-challenge-Damwi0Or.mjs";
4
4
  import { APIError } from "better-call";
5
5
  import { BetterAuthError } from "@better-auth/core/error";
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { p as oauthDeviceAuthorization, r as oauthProvider } from "./oauth-BWgg8X-A.mjs";
1
+ import { p as oauthDeviceAuthorization, r as oauthProvider } from "./oauth-ChlxgY8l.mjs";
2
2
  import * as _$_better_fetch_fetch0 from "@better-fetch/fetch";
3
3
 
4
4
  //#region src/client.d.ts
package/dist/client.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as buildSignedOAuthQuery } from "./signed-query-Df1MNiSH.mjs";
2
- import { t as PACKAGE_VERSION } from "./version-DVUrz-nv.mjs";
2
+ import { t as PACKAGE_VERSION } from "./version-DtVKOwOn.mjs";
3
3
  import { safeJSONParse } from "@better-auth/core/utils/json";
4
4
  import { deviceAuthorizationClient } from "better-auth/client/plugins";
5
5
  //#region src/client.ts
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as OAuthOpaqueAccessToken, B as Prompt, C as OAuthClientAuthenticationStrategy, D as OAuthExtensionGrantHandler, E as OAuthConsent, F as OAuthResource, G as VerificationValue, H as Scope, I as OAuthResourceInput, J as ResourceUriSchema, K as ClientRegistrationRequest, L as OAuthTokenIssueParams, M as OAuthProviderApi, N as OAuthProviderExtension, O as OAuthExtensionGrantHandlerInput, P as OAuthRefreshToken, R as OAuthTokenResponse, S as OAuthClientAuthenticationResult, T as OAuthClientResource, U as StoreTokenType, V as SchemaClient, W as StoredAuthorizationQuery, Y as oauthClientMetadataSchema, _ as OAuthAuthorizationQuery, a as GrantType, b as OAuthClientAuthenticationInput, c as ResourceServerMetadata, d as ActiveAccessTokenPayload, f as AuthorizePrompt, g as OAuthAuthenticatedClient, h as InitialAccessTokenAuthorization, i as Confirmation, j as OAuthOptions, k as OAuthMetadataExtensionInput, l as TokenEndpointAuthMethod, m as ClientMetadataResourceFetch, n as AuthServerMetadata, o as OAuthClient, p as ClientDiscovery, q as OAuthClientMetadata, r as BearerMethodsSupported, s as OIDCMetadata, t as AuthMethod, u as TokenType, v as OAuthClaimExtensionInput, w as OAuthClientRegistrationResponse, x as OAuthClientAuthenticationRequest, y as OAuthClientAdministrativeResponse, z as OAuthUserInfoExtensionInput } from "./oauth-DAMtT2vg.mjs";
2
- import { a as OAuthEndpointErrorResult, c as OAuthFieldErrorCode, d as DEVICE_CODE_GRANT_TYPE, f as OAuthDeviceAuthorizationOptions, i as getIssuer, l as OAuthFieldErrorCodeMap, n as getOAuthProviderState, o as OAuthEndpointRedirectContext, p as oauthDeviceAuthorization, r as oauthProvider, s as OAuthErrorCode, t as DEFAULT_OAUTH_SCOPES, u as OAuthRedirectOnError } from "./oauth-BWgg8X-A.mjs";
1
+ import { A as OAuthOpaqueAccessToken, B as Prompt, C as OAuthClientAuthenticationStrategy, D as OAuthExtensionGrantHandler, E as OAuthConsent, F as OAuthResource, G as VerificationValue, H as Scope, I as OAuthResourceInput, J as ResourceUriSchema, K as ClientRegistrationRequest, L as OAuthTokenIssueParams, M as OAuthProviderApi, N as OAuthProviderExtension, O as OAuthExtensionGrantHandlerInput, P as OAuthRefreshToken, R as OAuthTokenResponse, S as OAuthClientAuthenticationResult, T as OAuthClientResource, U as StoreTokenType, V as SchemaClient, W as StoredAuthorizationQuery, Y as oauthClientMetadataSchema, _ as OAuthAuthorizationQuery, a as GrantType, b as OAuthClientAuthenticationInput, c as ResourceServerMetadata, d as ActiveAccessTokenPayload, f as AuthorizePrompt, g as OAuthAuthenticatedClient, h as InitialAccessTokenAuthorization, i as Confirmation, j as OAuthOptions, k as OAuthMetadataExtensionInput, l as TokenEndpointAuthMethod, m as ClientMetadataResourceFetch, n as AuthServerMetadata, o as OAuthClient, p as ClientDiscovery, q as OAuthClientMetadata, r as BearerMethodsSupported, s as OIDCMetadata, t as AuthMethod, u as TokenType, v as OAuthClaimExtensionInput, w as OAuthClientRegistrationResponse, x as OAuthClientAuthenticationRequest, y as OAuthClientAdministrativeResponse, z as OAuthUserInfoExtensionInput } from "./oauth-DXiqbRzK.mjs";
2
+ import { a as OAuthEndpointErrorResult, c as OAuthFieldErrorCode, d as DEVICE_CODE_GRANT_TYPE, f as OAuthDeviceAuthorizationOptions, i as getIssuer, l as OAuthFieldErrorCodeMap, n as getOAuthProviderState, o as OAuthEndpointRedirectContext, p as oauthDeviceAuthorization, r as oauthProvider, s as OAuthErrorCode, t as DEFAULT_OAUTH_SCOPES, u as OAuthRedirectOnError } from "./oauth-ChlxgY8l.mjs";
3
3
  import { APIError } from "better-call";
4
4
  import { JWSAlgorithms, JwtOptions } from "better-auth/plugins";
5
5
  import { AuthContext, GenericEndpointContext } from "@better-auth/core";
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { T as oauthClientMetadataSchema, b as ResourceUriSchema, f as extractRepeatedResourceFromForm, i as getOAuthProviderApi, v as resolveResourcePolicy } from "./introspect-7WCMPe5Z.mjs";
2
- import { D as validateClientScopes, N as extendOAuthProvider, O as verifyOAuthQueryParams, T as toResourceList, a as getClient, f as normalizeClientAuthenticationParameters, s as getOAuthProviderPlugin, w as toAudienceClaim } from "./utils-DKU5wblX.mjs";
3
- import { d as metadataResponse, f as oauthAuthorizationServerMetadata, h as oidcServerMetadata, i as oauthProvider, m as oauthProviderOpenIdConfigMetadata, n as DEFAULT_OAUTH_SCOPES, p as oauthProviderAuthServerMetadata, r as getOAuthProviderState, s as consumeClientAssertion, t as getIssuer, u as authServerMetadata } from "./authorize-D8r6mvZx.mjs";
1
+ import { E as oauthClientMetadataSchema, i as getOAuthProviderApi, p as extractRepeatedResourceFromForm, x as ResourceUriSchema, y as resolveResourcePolicy } from "./introspect-B-5vOvYK.mjs";
2
+ import { L as extendOAuthProvider, M as verifyOAuthQueryParams, O as toAudienceClaim, g as normalizeClientAuthenticationParameters, j as validateClientScopes, k as toResourceList, s as getClient, u as getOAuthProviderPlugin } from "./utils-vIo_wPv5.mjs";
3
+ import { d as metadataResponse, f as oauthAuthorizationServerMetadata, h as oidcServerMetadata, i as oauthProvider, m as oauthProviderOpenIdConfigMetadata, n as DEFAULT_OAUTH_SCOPES, p as oauthProviderAuthServerMetadata, r as getOAuthProviderState, s as consumeClientAssertion, t as getIssuer, u as authServerMetadata } from "./authorize-CtCi8mAs.mjs";
4
4
  import { t as createResourceServerChallenge } from "./resource-challenge-Damwi0Or.mjs";
5
5
  import { APIError } from "better-auth/api";
6
6
  import * as z from "zod";
@@ -52,14 +52,14 @@ async function exchangeOAuthDeviceCode(input) {
52
52
  const body = ctx.body;
53
53
  const deviceCode = body?.device_code;
54
54
  if (!deviceCode) tokenError("BAD_REQUEST", "invalid_request", "device_code is required");
55
- const { authorizationContext: { client, confirmation, scopes }, redemptionContext: resources, user } = await redeemDeviceCode({
55
+ const authentication = await provider.authenticateClient({ requireCredentials: false });
56
+ const { authorizationContext: { client, confirmation, scopes }, claimedDeviceCode, redemptionContext: resources, user } = await redeemDeviceCode({
56
57
  ctx,
57
58
  deviceCode,
58
59
  authorizeRedemption: async (record) => {
59
60
  if (!record.oauthClientId) tokenError("BAD_REQUEST", "invalid_grant", "invalid device code");
60
61
  if (body?.client_id && record.oauthClientId !== body.client_id) tokenError("BAD_REQUEST", "invalid_grant", "Client ID mismatch");
61
62
  const scopes = parseScopes(record.scope);
62
- const authentication = await provider.authenticateClient({ requireCredentials: false });
63
63
  if (record.oauthClientId !== authentication.client.clientId) tokenError("BAD_REQUEST", "invalid_grant", "Client ID mismatch");
64
64
  validateClientScopes(authentication.client, scopes);
65
65
  return {
@@ -94,6 +94,7 @@ async function exchangeOAuthDeviceCode(input) {
94
94
  scopes,
95
95
  user,
96
96
  resources,
97
+ originalResources: claimedDeviceCode.resources ?? void 0,
97
98
  confirmation
98
99
  });
99
100
  }
@@ -1,26 +1,7 @@
1
- import { H as Scope, V as SchemaClient, j as OAuthOptions, o as OAuthClient } from "./oauth-DAMtT2vg.mjs";
2
- import { JSONWebKeySet } from "jose";
1
+ import { H as Scope, V as SchemaClient, j as OAuthOptions, o as OAuthClient } from "./oauth-DXiqbRzK.mjs";
2
+ import { n as PublicClientJwksValidationResult, r as validatePublicClientJwks } from "./client-jwks-DY2QBrsl.mjs";
3
3
  import { GenericEndpointContext } from "@better-auth/core";
4
4
 
5
- //#region src/client-jwks.d.ts
6
- type PublicClientJwksValidationResult = {
7
- valid: true;
8
- jwks: JSONWebKeySet;
9
- } | {
10
- valid: false;
11
- error: "jwks must be an RFC 7517 JWK Set object with a non-empty keys array" | "jwks must contain only public asymmetric keys" | "jwks keys must be supported public JWKs with required key parameters" | "jwks key alg must be supported for private_key_jwt and compatible with its key type and signing curve";
12
- };
13
- /**
14
- * Validates an OAuth client's public asymmetric JWK set.
15
- *
16
- * This boundary accepts only the RFC 7517 `{ keys: [...] }` representation.
17
- * It performs no I/O and returns the validated set for downstream JOSE
18
- * verification.
19
- *
20
- * @internal
21
- */
22
- declare function validatePublicClientJwks(input: unknown): PublicClientJwksValidationResult;
23
- //#endregion
24
5
  //#region src/client-metadata.d.ts
25
6
  declare function isForbiddenCimdClientMetadataField(field: string): boolean;
26
7
  //#endregion
package/dist/internal.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as registerClientMetadataDocument, c as isForbiddenCimdClientMetadataField, l as validatePublicClientJwks } from "./authorize-D8r6mvZx.mjs";
1
+ import { a as registerClientMetadataDocument, c as isForbiddenCimdClientMetadataField, l as validatePublicClientJwks } from "./authorize-CtCi8mAs.mjs";
2
2
  export { isForbiddenCimdClientMetadataField, registerClientMetadataDocument, validatePublicClientJwks };
@@ -1,12 +1,12 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
- import { A as collectExtensionAccessTokenClaims, C as storeToken, E as validateClientCredentials, F as getExtensionGrantHandler, L as getSupportedGrantTypes, M as collectExtensionUserInfoClaims, R as hasUserInfoClaimExtension, T as toResourceList, a as getClient, b as resolveSubjectIdentifier, c as getStoredToken, h as parseClientMetadata, i as extractClientCredentials, j as collectExtensionIdTokenClaims, l as isPKCERequired, n as decryptStoredClientSecret, o as getJwtPlugin, p as normalizeTimestampValue, r as destructureCredentials, t as clientAllowsGrant, w as toAudienceClaim, y as resolveSessionAuthTime } from "./utils-DKU5wblX.mjs";
2
+ import { A as validateClientCredentials, C as resolveSessionAuthTime, D as storeToken, F as collectExtensionIdTokenClaims, H as hasUserInfoClaimExtension, I as collectExtensionUserInfoClaims, O as toAudienceClaim, P as collectExtensionAccessTokenClaims, V as getSupportedGrantTypes, _ as normalizeTimestampValue, a as extractClientCredentials, d as getStoredToken, f as isInvalidJwtError, i as destructureCredentials, j as validateClientScopes, k as toResourceList, l as getJwtPlugin, n as clientAllowsGrant, p as isPKCERequired, r as decryptStoredClientSecret, s as getClient, w as resolveSubjectIdentifier, y as parseClientMetadata, z as getExtensionGrantHandler } from "./utils-vIo_wPv5.mjs";
3
3
  import { APIError } from "better-auth/api";
4
4
  import { generateRandomString, symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
5
5
  import { APIError as APIError$1 } from "better-call";
6
6
  import { logger } from "@better-auth/core/env";
7
7
  import * as z from "zod";
8
+ import { SignJWT, base64url, createLocalJWKSet, decodeProtectedHeader, errors, jwtVerify } from "jose";
8
9
  import { createDpopReplayStore, enforceDpopBinding, generateCodeChallenge, getConfirmationJkt, getDpopJktFromPayload, getJwks, isDpopBindingError, isDpopProofError, parseAccessTokenAuthorization, stripAccessTokenAuthorizationScheme, verifyDpopProof } from "better-auth/oauth2";
9
- import { SignJWT, base64url, createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
10
10
  import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
11
11
  import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
12
12
  //#region src/authentication-context.ts
@@ -1084,14 +1084,21 @@ function getDpopProofJwt(ctx) {
1084
1084
  return ctx.headers?.get("dpop") ?? void 0;
1085
1085
  }
1086
1086
  /**
1087
- * The endpoint's public URL, built from `baseURL`, which clients use as the
1088
- * DPoP `htu`. Not `request.url`: proxies and dev servers can rewrite its host.
1087
+ * The endpoint's public URL, which clients use as the DPoP `htu`: the origin
1088
+ * and base path of `baseURL` (not `request.url`, whose host proxies and dev
1089
+ * servers can rewrite) plus the request's own path below the base path, so
1090
+ * route params and a trailing slash match what the client called. `path` is
1091
+ * used when there is no HTTP request, such as an `auth.api` call.
1089
1092
  */
1090
1093
  function getEndpointUrl(ctx, path) {
1091
1094
  const { baseURL } = ctx.context;
1092
1095
  let end = baseURL.length;
1093
1096
  while (end > 0 && baseURL[end - 1] === "/") end--;
1094
- return `${baseURL.slice(0, end)}${path}`;
1097
+ const base = baseURL.slice(0, end);
1098
+ if (!ctx.request || !base) return `${base}${path}`;
1099
+ const basePath = new URL(base).pathname.replace(/\/$/, "");
1100
+ const requestPath = new URL(ctx.request.url).pathname;
1101
+ return `${base}${requestPath.startsWith(`${basePath}/`) ? requestPath.slice(basePath.length) : path}`;
1095
1102
  }
1096
1103
  //#endregion
1097
1104
  //#region src/oauthClient/client-credentials.ts
@@ -1971,6 +1978,10 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1971
1978
  error_description: "Either code_verifier or client_secret is required",
1972
1979
  error: "invalid_request"
1973
1980
  });
1981
+ /** Verify Client (RFC 6749 §5.2: an unknown or unauthenticated client is
1982
+ * `invalid_client`, so resolve it before the code lookup can report
1983
+ * `invalid_grant` and before a one-time code is consumed). */
1984
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, "authorization_code", authMethod);
1974
1985
  /** Get and check Verification Value */
1975
1986
  const { verificationValue, effectiveResources, authorizedResources, authorizationCodeId } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
1976
1987
  const scopes = verificationValue.query.scope?.split(" ");
@@ -1978,8 +1989,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1978
1989
  error_description: "verification scope unset",
1979
1990
  error: "invalid_scope"
1980
1991
  });
1981
- /** Verify Client */
1982
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerified, "authorization_code", authMethod);
1992
+ validateClientScopes(client, scopes);
1983
1993
  if (isPKCERequired(client, {
1984
1994
  scopes: (verificationValue.query?.scope)?.split(" ") || [],
1985
1995
  nonce: verificationValue.query?.nonce
@@ -1996,17 +2006,17 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1996
2006
  const pkceUsedInAuth = !!verificationValue.query?.code_challenge;
1997
2007
  const pkceUsedInToken = !!code_verifier;
1998
2008
  if (pkceUsedInAuth || pkceUsedInToken) {
1999
- if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
2009
+ if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("BAD_REQUEST", {
2000
2010
  error_description: "code_verifier required because PKCE was used in authorization",
2001
2011
  error: "invalid_request"
2002
2012
  });
2003
- if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
2013
+ if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("BAD_REQUEST", {
2004
2014
  error_description: "code_verifier provided but PKCE was not used in authorization",
2005
2015
  error: "invalid_request"
2006
2016
  });
2007
- if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("UNAUTHORIZED", {
2017
+ if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("BAD_REQUEST", {
2008
2018
  error_description: "code verification failed",
2009
- error: "invalid_request"
2019
+ error: "invalid_grant"
2010
2020
  });
2011
2021
  }
2012
2022
  /** Get user */
@@ -2109,6 +2119,10 @@ async function handleRefreshTokenGrant(ctx, opts) {
2109
2119
  error_description: "Missing a required refresh_token for refresh_token grant",
2110
2120
  error: "invalid_request"
2111
2121
  });
2122
+ /** Verify Client (RFC 6749 §5.2: an unknown or unauthenticated client is
2123
+ * `invalid_client`, so resolve it before the refresh token lookup can
2124
+ * report `invalid_grant`). */
2125
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, "refresh_token", authMethod);
2112
2126
  const decodedRefresh = await decodeRefreshToken(opts, refresh_token);
2113
2127
  const refreshToken = await ctx.context.adapter.findOne({
2114
2128
  model: "oauthRefreshToken",
@@ -2142,7 +2156,7 @@ async function handleRefreshTokenGrant(ctx, opts) {
2142
2156
  error: "invalid_scope"
2143
2157
  });
2144
2158
  }
2145
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes, preVerified, "refresh_token", authMethod);
2159
+ validateClientScopes(client, requestedScopes ?? scopes);
2146
2160
  if (refreshToken.revoked) {
2147
2161
  if (isWithinRefreshTokenReuseInterval(refreshToken)) {
2148
2162
  const replay = await getRefreshTokenRotationReplay(ctx, refreshToken, await resolveRefreshTokenRotationReplayRequest(ctx, opts, {
@@ -2235,29 +2249,29 @@ async function isIntrospectionAuthorized(ctx, opts, params) {
2235
2249
  * @returns RFC7662 introspection format
2236
2250
  */
2237
2251
  async function validateJwtAccessToken(ctx, opts, token, clientId) {
2238
- const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
2239
- const jwtPluginOptions = jwtPlugin?.options;
2252
+ if (opts.disableJwtPlugin) throw new APIError$1("BAD_REQUEST", {
2253
+ error_description: "JWT access tokens are disabled",
2254
+ error: "invalid_request"
2255
+ });
2256
+ const jwtPlugin = getJwtPlugin(ctx.context);
2257
+ const jwtPluginOptions = jwtPlugin.options;
2240
2258
  const userInfoAud = `${ctx.context.baseURL ?? ""}/oauth2/userinfo`;
2241
2259
  const expectedIssuer = jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL;
2242
2260
  let jwtPayload;
2243
2261
  try {
2244
2262
  jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
2245
2263
  jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
2246
- return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
2264
+ return (await jwtPlugin.endpoints.getJwks(ctx))?.response;
2247
2265
  },
2248
2266
  jwksCacheKey: jwtPlugin
2249
2267
  })), { issuer: expectedIssuer })).payload;
2250
2268
  } catch (error) {
2251
- if (error instanceof Error) {
2252
- if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
2253
- error_description: "invalid JWT signature",
2254
- error: "invalid_request"
2255
- });
2256
- else if (error.name === "JWTExpired") return { active: false };
2257
- else if (error.name === "JWTInvalid") return { active: false };
2258
- throw error;
2259
- }
2260
- throw new Error(error);
2269
+ if (error instanceof errors.JWTExpired || error instanceof errors.JWTInvalid) return { active: false };
2270
+ if (error instanceof TypeError || isInvalidJwtError(error)) throw new APIError$1("BAD_REQUEST", {
2271
+ error_description: "invalid JWT signature",
2272
+ error: "invalid_request"
2273
+ });
2274
+ throw error;
2261
2275
  }
2262
2276
  const rawAud = jwtPayload.aud;
2263
2277
  if (!await isAudienceClaimAllowed(ctx, opts, rawAud, [userInfoAud])) return { active: false };
@@ -2548,4 +2562,4 @@ async function introspectEndpoint(ctx, opts) {
2548
2562
  }
2549
2563
  }
2550
2564
  //#endregion
2551
- export { claimsRequestParameterSchema as A, clientJwksSchema as C, STANDARD_CLAIM_NAMES as D, STANDARD_CLAIMS as E, getRequestedUserInfoClaims as M, isValidOidcClaimsRequest as N, getSupportedClaims as O, authorizationQuerySchema as S, oauthClientMetadataSchema as T, logStartupSeedFailure as _, invalidateRefreshFamily as a, ResourceUriSchema as b, normalizeClientCredentialsScopes as c, assertIdentifierValid as d, extractRepeatedResourceFromForm as f, logEnforcePerClientResourcesResolution as g, isAudienceClaimAllowed as h, getOAuthProviderApi as i, filterClaimsRequestUserInfoClaims as j, canSatisfyEssentialAcrRequest as k, validateClientCredentialsScopes as l, invalidateResourceCache as m, introspect_exports as n, tokenEndpoint as o, getResource as p, decodeRefreshToken as r, userInfoEndpoint as s, introspectEndpoint as t, JWS_ALGORITHMS as u, resolveResourcePolicy as v, clientRegistrationRequestSchema as w, SafeUrlSchema as x, seedResourcesOnce as y };
2565
+ export { canSatisfyEssentialAcrRequest as A, authorizationQuerySchema as C, STANDARD_CLAIMS as D, oauthClientMetadataSchema as E, filterClaimsRequestUserInfoClaims as M, getRequestedUserInfoClaims as N, STANDARD_CLAIM_NAMES as O, isValidOidcClaimsRequest as P, SafeUrlSchema as S, clientRegistrationRequestSchema as T, logEnforcePerClientResourcesResolution as _, invalidateRefreshFamily as a, seedResourcesOnce as b, normalizeClientCredentialsScopes as c, JWS_ALGORITHMS as d, assertIdentifierValid as f, isAudienceClaimAllowed as g, invalidateResourceCache as h, getOAuthProviderApi as i, claimsRequestParameterSchema as j, getSupportedClaims as k, validateClientCredentialsScopes as l, getResource as m, introspect_exports as n, tokenEndpoint as o, extractRepeatedResourceFromForm as p, decodeRefreshToken as r, userInfoEndpoint as s, introspectEndpoint as t, getEndpointUrl as u, logStartupSeedFailure as v, clientJwksSchema as w, ResourceUriSchema as x, resolveResourcePolicy as y };
@@ -1,4 +1,5 @@
1
- import { B as Prompt, E as OAuthConsent, F as OAuthResource, H as Scope, O as OAuthExtensionGrantHandlerInput, R as OAuthTokenResponse, a as GrantType, j as OAuthOptions, k as OAuthMetadataExtensionInput, l as TokenEndpointAuthMethod, n as AuthServerMetadata, o as OAuthClient, u as TokenType, w as OAuthClientRegistrationResponse, y as OAuthClientAdministrativeResponse } from "./oauth-DAMtT2vg.mjs";
1
+ import { B as Prompt, E as OAuthConsent, F as OAuthResource, H as Scope, O as OAuthExtensionGrantHandlerInput, R as OAuthTokenResponse, a as GrantType, j as OAuthOptions, k as OAuthMetadataExtensionInput, l as TokenEndpointAuthMethod, n as AuthServerMetadata, o as OAuthClient, u as TokenType, w as OAuthClientRegistrationResponse, y as OAuthClientAdministrativeResponse } from "./oauth-DXiqbRzK.mjs";
2
+ import { t as ClientJwks } from "./client-jwks-DY2QBrsl.mjs";
2
3
  import * as _$better_call0 from "better-call";
3
4
  import * as z from "zod";
4
5
  import * as _$jose from "jose";
@@ -919,7 +920,12 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
919
920
  schema: {
920
921
  type: "object";
921
922
  properties: {
922
- redirect_uri: {
923
+ redirect: {
924
+ type: string;
925
+ description: string;
926
+ enum: boolean[];
927
+ };
928
+ url: {
923
929
  type: string;
924
930
  format: string;
925
931
  description: string;
@@ -979,7 +985,12 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
979
985
  schema: {
980
986
  type: "object";
981
987
  properties: {
982
- redirect_uri: {
988
+ redirect: {
989
+ type: string;
990
+ description: string;
991
+ enum: boolean[];
992
+ };
993
+ url: {
983
994
  type: string;
984
995
  format: string;
985
996
  description: string;
@@ -2386,6 +2397,30 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
2386
2397
  };
2387
2398
  };
2388
2399
  }, OAuthClient>;
2400
+ rotateClientKeys: _$better_call0.StrictEndpoint<"/oauth2/client/rotate-keys", {
2401
+ method: "POST";
2402
+ body: z.ZodObject<{
2403
+ client_assertion: z.ZodString;
2404
+ client_assertion_type: z.ZodString;
2405
+ jwks: z.ZodObject<{
2406
+ keys: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2407
+ }, z.core.$strip>;
2408
+ }, z.core.$strip>;
2409
+ errorCodesByField: {
2410
+ client_assertion: "invalid_client";
2411
+ client_assertion_type: "invalid_client";
2412
+ jwks: "invalid_client_metadata";
2413
+ };
2414
+ metadata: {
2415
+ noStore: boolean;
2416
+ openapi: {
2417
+ description: string;
2418
+ };
2419
+ };
2420
+ }, {
2421
+ client_id: string;
2422
+ jwks: ClientJwks;
2423
+ }>;
2389
2424
  deleteOAuthClient: _$better_call0.StrictEndpoint<"/oauth2/delete-client", {
2390
2425
  method: "POST";
2391
2426
  use: _$better_call0.Middleware<_$better_call0.MiddlewareOptions, (inputContext: _$better_call0.MiddlewareInputContext<_$better_call0.MiddlewareOptions>) => Promise<{
@@ -3144,6 +3179,10 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
3144
3179
  pathMatcher: (path: string) => path is "/oauth2/register";
3145
3180
  window: number;
3146
3181
  max: number;
3182
+ } | {
3183
+ pathMatcher: (path: string) => path is "/oauth2/client/rotate-keys";
3184
+ window: number;
3185
+ max: number;
3147
3186
  } | {
3148
3187
  pathMatcher: (path: string) => path is "/oauth2/userinfo";
3149
3188
  window: number;
@@ -1316,6 +1316,21 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
1316
1316
  * @default 300 (5 minutes)
1317
1317
  */
1318
1318
  assertionMaxLifetime?: number;
1319
+ /**
1320
+ * Lets a confidential `private_key_jwt` client replace its own inline `jwks`
1321
+ * at `POST /oauth2/client/rotate-keys`, authenticated by a client assertion
1322
+ * signed with one of its current keys. No user session is involved.
1323
+ *
1324
+ * Clients that publish their keys at a `jwks_uri` rotate them there, so they
1325
+ * cannot use this endpoint.
1326
+ *
1327
+ * Each rotation assertion must be single-use, so the provider refuses to
1328
+ * start with this enabled on the memory adapter or with
1329
+ * `advanced.database.generateId: "serial"`, neither of which enforces it.
1330
+ *
1331
+ * @default false
1332
+ */
1333
+ allowClientKeyRotation?: boolean;
1319
1334
  /**
1320
1335
  * Allows /oauth2/public-client-prelogin endpoint to be
1321
1336
  * requestable prior to login via a valid oauth_query.
@@ -1434,14 +1449,16 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
1434
1449
  * field.
1435
1450
  *
1436
1451
  * - `"web"`: the OIDC Registration §2 default. Omitted `application_type` is
1437
- * treated as `web`, so custom-scheme redirect URIs are rejected.
1452
+ * treated as `web`, so custom-scheme and loopback redirect URIs are
1453
+ * rejected.
1438
1454
  * - `"native"`: omitted `application_type` is treated as `native`.
1439
1455
  * - `"infer"`: omitted `application_type` is treated as `native` when any
1440
- * `redirect_uris` entry uses a non-http(s) scheme; otherwise `web`. Every
1441
- * redirect URI is then validated against the native rules. Lets native MCP
1442
- * clients that omit `application_type` (for example Cursor, which also
1443
- * registers https and loopback callbacks) register without loosening
1444
- * web-client redirect policy.
1456
+ * `redirect_uris` entry uses a non-http(s) scheme or is `http` on
1457
+ * `localhost`, `127.0.0.1`, or `[::1]`; otherwise `web`. Every redirect URI
1458
+ * of an inferred native client is then validated against the native rules.
1459
+ * Lets native MCP clients that omit `application_type` (for example Cursor,
1460
+ * or older Claude Code releases with an `http://127.0.0.1` callback)
1461
+ * register without loosening web-client redirect policy.
1445
1462
  *
1446
1463
  * An `application_type` the client actually sends is never overridden.
1447
1464
  * Applies only to dynamic client registration; managed clients and Client ID
@@ -1626,7 +1643,8 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
1626
1643
  */
1627
1644
  page: string;
1628
1645
  /**
1629
- * A value to tie to the consent reference_id.
1646
+ * A value to tie to the consent reference_id. Consent is stored
1647
+ * per reference; `undefined` or `""` uses consent given without one.
1630
1648
  *
1631
1649
  * Note that YOU must fail in this function if the requested
1632
1650
  * scope doesn't have a reference id and it should.
@@ -1937,6 +1955,7 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
1937
1955
  * introspect: { window: 60, max: 100 },
1938
1956
  * revoke: { window: 60, max: 30 },
1939
1957
  * register: { window: 60, max: 5 },
1958
+ * rotateClientKeys: { window: 60, max: 5 },
1940
1959
  * userinfo: { window: 60, max: 60 },
1941
1960
  * }
1942
1961
  * ```
@@ -1982,6 +2001,14 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
1982
2001
  window: number;
1983
2002
  max: number;
1984
2003
  } | false;
2004
+ /**
2005
+ * Rate limit for /oauth2/client/rotate-keys endpoint
2006
+ * @default { window: 60, max: 5 }
2007
+ */
2008
+ rotateClientKeys?: {
2009
+ window: number;
2010
+ max: number;
2011
+ } | false;
1985
2012
  /**
1986
2013
  * Rate limit for /oauth2/userinfo endpoint
1987
2014
  * @default { window: 60, max: 60 }
@@ -6,6 +6,7 @@ import { BetterAuthError } from "@better-auth/core/error";
6
6
  import { CLIENT_ASSERTION_TYPE, decodeBasicCredentials } from "@better-auth/core/oauth2";
7
7
  import { base64Url } from "@better-auth/utils/base64";
8
8
  import { createHash } from "@better-auth/utils/hash";
9
+ import { errors } from "jose";
9
10
  //#region src/extensions.ts
10
11
  const DEFAULT_GRANT_TYPES = [
11
12
  "authorization_code",
@@ -294,6 +295,21 @@ const getJwtPlugin = (ctx) => {
294
295
  if (!plugin) throw new BetterAuthError("jwt_config");
295
296
  return plugin;
296
297
  };
298
+ const joseInfrastructureErrorCodes = new Set([
299
+ errors.JWKSTimeout.code,
300
+ errors.JWKSInvalid.code,
301
+ errors.JWKSMultipleMatchingKeys.code
302
+ ]);
303
+ /**
304
+ * Whether a JWT verification error comes from the token itself (malformed,
305
+ * bad signature, unsupported alg, unknown kid, failed claim checks), as
306
+ * opposed to a key set failure that should still surface as a server error.
307
+ *
308
+ * @internal
309
+ */
310
+ function isInvalidJwtError(error) {
311
+ return error instanceof errors.JOSEError && !joseInfrastructureErrorCodes.has(error.code);
312
+ }
297
313
  /**
298
314
  * Normalizes timestamp-like values returned by adapters.
299
315
  *
@@ -371,6 +387,16 @@ async function verifyOAuthQueryParams(oauth_query, secret) {
371
387
  return sigs.length === 1 && !!sig && constantTimeEqual(sig, verifySig) && /* @__PURE__ */ new Date(exp * 1e3) >= /* @__PURE__ */ new Date();
372
388
  }
373
389
  /**
390
+ * Rejects changes to a client listed in `cachedTrustedClients`. Cached trusted
391
+ * clients are immutable through the client management endpoints.
392
+ */
393
+ function assertNotTrustedClient(opts, clientId) {
394
+ if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("BAD_REQUEST", {
395
+ error: "invalid_client",
396
+ error_description: "trusted clients must be updated manually"
397
+ });
398
+ }
399
+ /**
374
400
  * Get a client by ID, checking trusted clients first, then database
375
401
  */
376
402
  async function getClient(ctx, options, clientId) {
@@ -640,7 +666,7 @@ function throwInvalidClient(errorDescription, authentication) {
640
666
  */
641
667
  async function validateClientCredentials(ctx, options, clientId, clientSecret, scopes, preVerified, grantType, authMethod) {
642
668
  const client = await getClient(ctx, options, clientId);
643
- if (!client) throwInvalidClient("missing client", { method: authMethod });
669
+ if (!client) throwInvalidClient("client not found", { method: authMethod });
644
670
  if (client.disabled) throwInvalidClient("client is disabled", { method: authMethod });
645
671
  const registeredAuthMethod = client.tokenEndpointAuthMethod ?? "client_secret_basic";
646
672
  if (authMethod && registeredAuthMethod !== authMethod) throwInvalidClient(`client registered for ${registeredAuthMethod} cannot use ${authMethod}`, { method: authMethod });
@@ -712,7 +738,7 @@ async function extractClientCredentials(ctx, opts, expectedAudience) {
712
738
  confirmation: result.confirmation
713
739
  };
714
740
  }
715
- const { verifyClientAssertion: verify } = await import("./authorize-D8r6mvZx.mjs").then((n) => n.o);
741
+ const { verifyClientAssertion: verify } = await import("./authorize-CtCi8mAs.mjs").then((n) => n.o);
716
742
  return {
717
743
  kind: "pre_verified",
718
744
  method: "private_key_jwt",
@@ -791,6 +817,51 @@ async function resolveSubjectIdentifier(userId, client, opts) {
791
817
  return userId;
792
818
  }
793
819
  /**
820
+ * Resolves `postLogin.consentReferenceId`, treating an empty value as no reference.
821
+ *
822
+ * @internal
823
+ */
824
+ async function getConsentReferenceId(opts, context) {
825
+ return await opts.postLogin?.consentReferenceId?.(context) || void 0;
826
+ }
827
+ /**
828
+ * Finds the consent stored for exactly this reference. Without a reference,
829
+ * only consent stored without one (missing, null, or empty) matches.
830
+ *
831
+ * @internal
832
+ */
833
+ async function findConsent(ctx, clientId, userId, referenceId) {
834
+ const where = [{
835
+ field: "clientId",
836
+ value: clientId
837
+ }, {
838
+ field: "userId",
839
+ value: userId
840
+ }];
841
+ if (referenceId) return ctx.context.adapter.findOne({
842
+ model: "oauthConsent",
843
+ where: [...where, {
844
+ field: "referenceId",
845
+ value: referenceId
846
+ }]
847
+ });
848
+ const pageSize = 100;
849
+ for (let offset = 0;; offset += pageSize) {
850
+ const consents = await ctx.context.adapter.findMany({
851
+ model: "oauthConsent",
852
+ where,
853
+ limit: pageSize,
854
+ offset,
855
+ sortBy: {
856
+ field: "id",
857
+ direction: "asc"
858
+ }
859
+ });
860
+ const consent = consents.find((row) => !row.referenceId);
861
+ if (consent || consents.length < pageSize) return consent ?? null;
862
+ }
863
+ }
864
+ /**
794
865
  * Converts URLSearchParams to a plain object, preserving
795
866
  * multi-valued keys as arrays instead of discarding duplicates.
796
867
  */
@@ -849,4 +920,4 @@ function isPKCERequired(client, request) {
849
920
  return false;
850
921
  }
851
922
  //#endregion
852
- export { collectExtensionAccessTokenClaims as A, validateOAuthProviderExtensions as B, storeToken as C, validateClientScopes as D, validateClientCredentials as E, getExtensionGrantHandler as F, getSupportedAuthMethods as I, getSupportedGrantTypes as L, collectExtensionUserInfoClaims as M, extendOAuthProvider as N, verifyOAuthQueryParams as O, getClientDiscoveries as P, hasUserInfoClaimExtension as R, storeClientSecret as S, toResourceList as T, removeMaxAgeFromQuery as _, getClient as a, resolveSubjectIdentifier as b, getStoredToken as c, mergeDiscoveryMetadata as d, normalizeClientAuthenticationParameters as f, parsePrompt as g, parseClientMetadata as h, extractClientCredentials as i, collectExtensionIdTokenClaims as j, applyOAuthProviderMetadataExtensions as k, isPKCERequired as l, parseBearerToken as m, decryptStoredClientSecret as n, getJwtPlugin as o, normalizeTimestampValue as p, destructureCredentials as r, getOAuthProviderPlugin as s, clientAllowsGrant as t, isSessionFreshForSignedQuery as u, removePromptFromQuery as v, toAudienceClaim as w, searchParamsToQuery as x, resolveSessionAuthTime as y, isExtensionTokenEndpointAuthMethod as z };
923
+ export { validateClientCredentials as A, getSupportedAuthMethods as B, resolveSessionAuthTime as C, storeToken as D, storeClientSecret as E, collectExtensionIdTokenClaims as F, hasUserInfoClaimExtension as H, collectExtensionUserInfoClaims as I, extendOAuthProvider as L, verifyOAuthQueryParams as M, applyOAuthProviderMetadataExtensions as N, toAudienceClaim as O, collectExtensionAccessTokenClaims as P, getClientDiscoveries as R, removePromptFromQuery as S, searchParamsToQuery as T, isExtensionTokenEndpointAuthMethod as U, getSupportedGrantTypes as V, validateOAuthProviderExtensions as W, normalizeTimestampValue as _, extractClientCredentials as a, parsePrompt as b, getConsentReferenceId as c, getStoredToken as d, isInvalidJwtError as f, normalizeClientAuthenticationParameters as g, mergeDiscoveryMetadata as h, destructureCredentials as i, validateClientScopes as j, toResourceList as k, getJwtPlugin as l, isSessionFreshForSignedQuery as m, clientAllowsGrant as n, findConsent as o, isPKCERequired as p, decryptStoredClientSecret as r, getClient as s, assertNotTrustedClient as t, getOAuthProviderPlugin as u, parseBearerToken as v, resolveSubjectIdentifier as w, removeMaxAgeFromQuery as x, parseClientMetadata as y, getExtensionGrantHandler as z };
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/version.ts
3
- const PACKAGE_VERSION = "1.7.9";
3
+ const PACKAGE_VERSION = "1.7.11";
4
4
  //#endregion
5
5
  export { PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/oauth-provider",
3
- "version": "1.7.9",
3
+ "version": "1.7.11",
4
4
  "description": "An oauth provider plugin for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -71,16 +71,16 @@
71
71
  "devDependencies": {
72
72
  "listhen": "^1.9.0",
73
73
  "tsdown": "0.21.10",
74
- "@better-auth/core": "1.7.9",
75
- "@better-auth/memory-adapter": "1.7.9",
76
- "better-auth": "1.7.9"
74
+ "@better-auth/core": "1.7.11",
75
+ "@better-auth/memory-adapter": "1.7.11",
76
+ "better-auth": "1.7.11"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "@better-auth/utils": "^0.4.2 || ^0.5.0",
80
80
  "@better-fetch/fetch": "1.3.2",
81
81
  "better-call": "1.4.2",
82
- "@better-auth/core": "^1.7.9",
83
- "better-auth": "^1.7.9"
82
+ "@better-auth/core": "^1.7.11",
83
+ "better-auth": "^1.7.11"
84
84
  },
85
85
  "scripts": {
86
86
  "build": "tsdown",