@better-auth/oauth-provider 1.7.9 → 1.7.10

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-BVm_zFQC.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-DfMNRh3J.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-aKfgWHzo.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",
@@ -2421,10 +2447,7 @@ async function deleteClientEndpoint(ctx, opts) {
2421
2447
  await assertClientPrivileges(ctx, session, opts, "delete", ctx.body.client_id);
2422
2448
  if (!session) throw new APIError("UNAUTHORIZED");
2423
2449
  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
- });
2450
+ assertNotTrustedClient(opts, clientId);
2428
2451
  const client = await getClient(ctx, opts, clientId);
2429
2452
  if (!client) throw new APIError("NOT_FOUND", {
2430
2453
  error_description: "client not found",
@@ -2448,10 +2471,7 @@ async function updateClientEndpoint(ctx, opts, settings) {
2448
2471
  await assertClientPrivileges(ctx, session, opts, "update", ctx.body.client_id);
2449
2472
  if (!session) throw new APIError("UNAUTHORIZED");
2450
2473
  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
- });
2474
+ assertNotTrustedClient(opts, clientId);
2455
2475
  const client = await getClient(ctx, opts, clientId);
2456
2476
  if (!client) throw new APIError("NOT_FOUND", {
2457
2477
  error_description: "client not found",
@@ -2519,10 +2539,7 @@ async function rotateClientSecretEndpoint(ctx, opts) {
2519
2539
  await assertClientPrivileges(ctx, session, opts, "rotate", ctx.body.client_id);
2520
2540
  if (!session) throw new APIError("UNAUTHORIZED");
2521
2541
  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
- });
2542
+ assertNotTrustedClient(opts, clientId);
2526
2543
  const client = await getClient(ctx, opts, clientId);
2527
2544
  if (!client) throw new APIError("NOT_FOUND", {
2528
2545
  error_description: "client not found",
@@ -2559,6 +2576,57 @@ async function rotateClientSecretEndpoint(ctx, opts) {
2559
2576
  clientSecret: (opts.prefix?.clientSecret ?? "") + clientSecret
2560
2577
  });
2561
2578
  }
2579
+ /**
2580
+ * The write is guarded on the exact key set the assertion was verified against.
2581
+ * A concurrent rotation changes that value, so the stale request loses instead
2582
+ * of overwriting a newer key set.
2583
+ *
2584
+ * @see https://datatracker.ietf.org/doc/html/rfc7523#section-3
2585
+ */
2586
+ async function rotateClientKeysEndpoint(ctx, opts) {
2587
+ if (!opts.allowClientKeyRotation) throw new APIError("FORBIDDEN", {
2588
+ error: "access_denied",
2589
+ error_description: "Client key rotation is disabled"
2590
+ });
2591
+ const { body } = ctx;
2592
+ assertValidClientJwks(body.jwks);
2593
+ const { client } = await verifyClientAssertion(ctx, opts, body.client_assertion, body.client_assertion_type, void 0, getEndpointUrl({
2594
+ context: ctx.context,
2595
+ request: void 0
2596
+ }, "/oauth2/client/rotate-keys"), {
2597
+ endpointAudienceOnly: true,
2598
+ assertClient: (candidate) => assertRotatable(opts, candidate)
2599
+ });
2600
+ if (!await ctx.context.adapter.incrementOne({
2601
+ model: "oauthClient",
2602
+ where: [{
2603
+ field: "clientId",
2604
+ value: client.clientId
2605
+ }, {
2606
+ field: "jwks",
2607
+ value: client.jwks
2608
+ }],
2609
+ increment: {},
2610
+ set: {
2611
+ jwks: JSON.stringify(body.jwks),
2612
+ updatedAt: /* @__PURE__ */ new Date()
2613
+ }
2614
+ })) throw new APIError("CONFLICT", {
2615
+ error: "invalid_request",
2616
+ error_description: "client keys changed while rotating; retry with a fresh client assertion"
2617
+ });
2618
+ return {
2619
+ client_id: client.clientId,
2620
+ jwks: body.jwks
2621
+ };
2622
+ }
2623
+ function assertRotatable(opts, client) {
2624
+ assertNotTrustedClient(opts, client.clientId);
2625
+ if (client.clientDiscoveryId || client.jwksUri || !client.jwks) throw new APIError("BAD_REQUEST", {
2626
+ error: "invalid_client",
2627
+ error_description: "client keys are not stored inline; rotate them where the client publishes them"
2628
+ });
2629
+ }
2562
2630
  //#endregion
2563
2631
  //#region src/oauthClient/index.ts
2564
2632
  const tokenEndpointAuthMethodSchema = z.string().trim().min(1);
@@ -3003,6 +3071,29 @@ const rotateClientSecret = (opts) => createAuthEndpoint("/oauth2/client/rotate-s
3003
3071
  }, async (ctx) => {
3004
3072
  return rotateClientSecretEndpoint(ctx, opts);
3005
3073
  });
3074
+ /**
3075
+ * Replaces a `private_key_jwt` client's own signing keys. The client proves it
3076
+ * holds a current key with a client assertion, so no session is needed.
3077
+ */
3078
+ const rotateClientKeys = (opts) => createOAuthEndpoint("/oauth2/client/rotate-keys", {
3079
+ method: "POST",
3080
+ body: z.object({
3081
+ client_assertion: z.string(),
3082
+ client_assertion_type: z.string(),
3083
+ jwks: clientJwksSchema
3084
+ }),
3085
+ errorCodesByField: {
3086
+ client_assertion: "invalid_client",
3087
+ client_assertion_type: "invalid_client",
3088
+ jwks: "invalid_client_metadata"
3089
+ },
3090
+ metadata: {
3091
+ noStore: true,
3092
+ 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." }
3093
+ }
3094
+ }, async (ctx) => {
3095
+ return rotateClientKeysEndpoint(ctx, opts);
3096
+ });
3006
3097
  const deleteOAuthClient = (opts) => createAuthEndpoint("/oauth2/delete-client", {
3007
3098
  method: "POST",
3008
3099
  use: [sessionMiddleware],
@@ -3468,29 +3559,30 @@ const adminUnlinkClientResource = (opts) => createAuthEndpoint("/admin/oauth2/re
3468
3559
  * introspection and userinfo.
3469
3560
  */
3470
3561
  async function revokeJwtAccessToken(ctx, opts, token) {
3471
- const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
3472
- const jwtPluginOptions = jwtPlugin?.options;
3562
+ if (opts.disableJwtPlugin) throw new APIError$1("BAD_REQUEST", {
3563
+ error_description: "JWT access tokens are disabled",
3564
+ error: "invalid_request"
3565
+ });
3566
+ const jwtPlugin = getJwtPlugin(ctx.context);
3567
+ const jwtPluginOptions = jwtPlugin.options;
3568
+ let jwtPayload;
3473
3569
  try {
3474
- const verified = await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
3570
+ jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
3475
3571
  jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
3476
- return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
3572
+ return (await jwtPlugin.endpoints.getJwks(ctx))?.response;
3477
3573
  },
3478
3574
  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;
3575
+ })), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL })).payload;
3482
3576
  } 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);
3577
+ if (error instanceof errors.JWTExpired || error instanceof errors.JWTInvalid) return null;
3578
+ if (error instanceof TypeError || isInvalidJwtError(error)) throw new APIError$1("BAD_REQUEST", {
3579
+ error_description: "invalid JWT signature",
3580
+ error: "invalid_request"
3581
+ });
3582
+ throw error;
3493
3583
  }
3584
+ const userInfoAudience = `${ctx.context.baseURL}/oauth2/userinfo`;
3585
+ if (!jwtPayload.azp || !await isAudienceClaimAllowed(ctx, opts, jwtPayload.aud, [userInfoAudience])) return null;
3494
3586
  throw new APIError$1("BAD_REQUEST", {
3495
3587
  error_description: "JWT access tokens are self-contained and cannot be revoked server-side",
3496
3588
  error: "unsupported_token_type"
@@ -4443,6 +4535,10 @@ const oauthProvider = (options) => {
4443
4535
  onRequest: handleIssuerMetadataRequest,
4444
4536
  init: async (ctx) => {
4445
4537
  if (ctx.options.secondaryStorage && ctx.options.session?.storeSessionInDatabase !== true) throw new BetterAuthError("OAuth Provider requires `session.storeSessionInDatabase: true` when using secondaryStorage");
4538
+ if (opts.allowClientKeyRotation) {
4539
+ const gap = ctx.adapter.id === "memory" ? "the memory adapter" : ctx.options.advanced?.database?.generateId === "serial" ? "`advanced.database.generateId: \"serial\"`" : void 0;
4540
+ if (gap) throw new BetterAuthError(`\`allowClientKeyRotation\` requires single-use client assertions, which ${gap} does not enforce`);
4541
+ }
4446
4542
  await seedResourcesOnce(ctx, opts).catch((error) => logStartupSeedFailure(ctx, error));
4447
4543
  logEnforcePerClientResourcesResolution(opts);
4448
4544
  if (!opts.disableJwtPlugin) {
@@ -5257,6 +5353,7 @@ const oauthProvider = (options) => {
5257
5353
  adminUpdateOAuthClient: adminUpdateOAuthClient(opts),
5258
5354
  updateOAuthClient: updateOAuthClient(opts),
5259
5355
  rotateClientSecret: rotateClientSecret(opts),
5356
+ rotateClientKeys: rotateClientKeys(opts),
5260
5357
  deleteOAuthClient: deleteOAuthClient(opts),
5261
5358
  getOAuthConsent: getOAuthConsent(opts),
5262
5359
  getOAuthConsents: getOAuthConsents(opts),
@@ -5297,6 +5394,11 @@ const oauthProvider = (options) => {
5297
5394
  window: opts.rateLimit?.register?.window ?? 60,
5298
5395
  max: opts.rateLimit?.register?.max ?? 5
5299
5396
  }] : [],
5397
+ ...opts.rateLimit?.rotateClientKeys !== false ? [{
5398
+ pathMatcher: (path) => path === "/oauth2/client/rotate-keys",
5399
+ window: opts.rateLimit?.rotateClientKeys?.window ?? 60,
5400
+ max: opts.rateLimit?.rotateClientKeys?.max ?? 5
5401
+ }] : [],
5300
5402
  ...opts.rateLimit?.userinfo !== false ? [{
5301
5403
  pathMatcher: (path) => path === "/oauth2/userinfo",
5302
5404
  window: opts.rateLimit?.userinfo?.window ?? 60,
@@ -5606,7 +5708,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
5606
5708
  if (promptSet?.has("select_account") && !opts.selectAccount?.page) return handleRedirect(ctx, getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
5607
5709
  if (!(query.response_type === "code")) return handleRedirect(ctx, getErrorURL(ctx, "unsupported_response_type", "unsupported response type"));
5608
5710
  const client = await getClient(ctx, opts, query.client_id);
5609
- if (!client) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "client_id is required"));
5711
+ if (!client) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "client not found"));
5610
5712
  if (client.disabled) return handleRedirect(ctx, getErrorURL(ctx, "client_disabled", "client is disabled"));
5611
5713
  if (!clientAllowsGrant(client, "authorization_code")) return handleRedirect(ctx, getErrorURL(ctx, "unauthorized_client", "client is not authorized to use the authorization_code grant"));
5612
5714
  const registeredUris = client.redirectUris ?? [];
@@ -5712,7 +5814,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
5712
5814
  }
5713
5815
  }
5714
5816
  if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
5715
- const referenceId = await opts.postLogin?.consentReferenceId?.({
5817
+ const referenceId = await getConsentReferenceId(opts, {
5716
5818
  user: session.user,
5717
5819
  session: session.session,
5718
5820
  scopes: requestedScopes
@@ -5726,23 +5828,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
5726
5828
  referenceId,
5727
5829
  resource: requestedResources
5728
5830
  });
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
- });
5831
+ const consent = await findConsent(ctx, client.clientId, session.user.id, referenceId);
5746
5832
  if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val)) || !requestedUserInfoClaims.every((claim) => (consent.requestedUserInfoClaims ?? []).includes(claim))) {
5747
5833
  if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "consent_required", "End-User consent is required");
5748
5834
  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-DfMNRh3J.mjs";
2
+ import { t as PACKAGE_VERSION } from "./version-aKfgWHzo.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-Cse_eZ6i.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-aKfgWHzo.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-Cse_eZ6i.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-BVm_zFQC.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-DfMNRh3J.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-dkOG8GqZ.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-dkOG8GqZ.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-DfMNRh3J.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";
@@ -2386,6 +2387,30 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
2386
2387
  };
2387
2388
  };
2388
2389
  }, OAuthClient>;
2390
+ rotateClientKeys: _$better_call0.StrictEndpoint<"/oauth2/client/rotate-keys", {
2391
+ method: "POST";
2392
+ body: z.ZodObject<{
2393
+ client_assertion: z.ZodString;
2394
+ client_assertion_type: z.ZodString;
2395
+ jwks: z.ZodObject<{
2396
+ keys: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2397
+ }, z.core.$strip>;
2398
+ }, z.core.$strip>;
2399
+ errorCodesByField: {
2400
+ client_assertion: "invalid_client";
2401
+ client_assertion_type: "invalid_client";
2402
+ jwks: "invalid_client_metadata";
2403
+ };
2404
+ metadata: {
2405
+ noStore: boolean;
2406
+ openapi: {
2407
+ description: string;
2408
+ };
2409
+ };
2410
+ }, {
2411
+ client_id: string;
2412
+ jwks: ClientJwks;
2413
+ }>;
2389
2414
  deleteOAuthClient: _$better_call0.StrictEndpoint<"/oauth2/delete-client", {
2390
2415
  method: "POST";
2391
2416
  use: _$better_call0.Middleware<_$better_call0.MiddlewareOptions, (inputContext: _$better_call0.MiddlewareInputContext<_$better_call0.MiddlewareOptions>) => Promise<{
@@ -3144,6 +3169,10 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
3144
3169
  pathMatcher: (path: string) => path is "/oauth2/register";
3145
3170
  window: number;
3146
3171
  max: number;
3172
+ } | {
3173
+ pathMatcher: (path: string) => path is "/oauth2/client/rotate-keys";
3174
+ window: number;
3175
+ max: number;
3147
3176
  } | {
3148
3177
  pathMatcher: (path: string) => path is "/oauth2/userinfo";
3149
3178
  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-dkOG8GqZ.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.10";
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.10",
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.10",
75
+ "@better-auth/memory-adapter": "1.7.10",
76
+ "better-auth": "1.7.10"
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.10",
83
+ "better-auth": "^1.7.10"
84
84
  },
85
85
  "scripts": {
86
86
  "build": "tsdown",