@better-auth/oauth-provider 1.7.8 → 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-H3uoUiuS.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-CGIZS-Da.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-BPS0os17.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 = {
@@ -175,7 +159,10 @@ async function postLogin(ctx, authorize) {
175
159
  ctx.headers?.set("accept", "application/json");
176
160
  ctx.query = searchParamsToQuery(query);
177
161
  const session = await getSessionFromCtx(ctx);
178
- return await authorize(ctx, { postLogin: state?.postLoginClearedForSession !== void 0 && state.postLoginClearedForSession === session?.session.id });
162
+ return await authorize(ctx, {
163
+ postLogin: state?.postLoginClearedForSession !== void 0 && state.postLoginClearedForSession === session?.session.id,
164
+ isPostLoginContinuation: true
165
+ });
179
166
  }
180
167
  //#endregion
181
168
  //#region src/logout.ts
@@ -1029,6 +1016,17 @@ function validatePublicClientJwks(input) {
1029
1016
  jwks: { keys }
1030
1017
  };
1031
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
+ }
1032
1030
  //#endregion
1033
1031
  //#region src/client-metadata.ts
1034
1032
  const OAUTH_CLIENT_RECORD_FIELD_NAMES = Object.freeze(Object.keys({
@@ -1489,8 +1487,20 @@ async function consumeClientAssertion(ctx, opts, params) {
1489
1487
  * Validates: signature, iss=client_id, sub=client_id, aud=the endpoint serving
1490
1488
  * the assertion or the normalized OP issuer, exp, assertion max lifetime,
1491
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`.
1492
1502
  */
1493
- async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertionType, clientIdHint, expectedAudience) {
1503
+ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertionType, clientIdHint, expectedAudience, options) {
1494
1504
  if (clientAssertionType !== CLIENT_ASSERTION_TYPE) throw new APIError$1("BAD_REQUEST", {
1495
1505
  error_description: "unsupported client_assertion_type",
1496
1506
  error: "invalid_client"
@@ -1528,7 +1538,7 @@ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertion
1528
1538
  });
1529
1539
  const client = await getClient(ctx, opts, clientId);
1530
1540
  if (!client) throw new APIError$1("BAD_REQUEST", {
1531
- error_description: "unknown client",
1541
+ error_description: "client not found",
1532
1542
  error: "invalid_client"
1533
1543
  });
1534
1544
  if (client.disabled) throw new APIError$1("BAD_REQUEST", {
@@ -1539,10 +1549,11 @@ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertion
1539
1549
  error_description: "client is not registered for private_key_jwt authentication",
1540
1550
  error: "invalid_client"
1541
1551
  });
1552
+ options?.assertClient?.(client);
1542
1553
  const jwks = await fetchClientJwks(ctx, opts, client);
1543
1554
  const endpointAudience = expectedAudience ?? `${ctx.context.baseURL}/oauth2/token`;
1544
1555
  const issuerAudience = getIssuer(ctx, opts);
1545
- const acceptedAudiences = [...new Set([endpointAudience, issuerAudience])];
1556
+ const acceptedAudiences = options?.endpointAudienceOnly ? [endpointAudience] : [...new Set([endpointAudience, issuerAudience])];
1546
1557
  const verifyOpts = {
1547
1558
  issuer: clientId,
1548
1559
  subject: clientId,
@@ -1578,7 +1589,10 @@ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertion
1578
1589
  payload,
1579
1590
  expectedAudience: acceptedAudience
1580
1591
  });
1581
- return { clientId };
1592
+ return {
1593
+ clientId,
1594
+ client
1595
+ };
1582
1596
  }
1583
1597
  //#endregion
1584
1598
  //#region src/utils/initial-access-token.ts
@@ -1656,6 +1670,21 @@ function resolveRegistrationResponseTypes(client, grantTypes) {
1656
1670
  if (client.response_types) return client.response_types;
1657
1671
  return grantTypes.includes("authorization_code") ? ["code"] : void 0;
1658
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
+ }
1659
1688
  function applyOAuthClientRegistrationDefaults(client, defaultApplicationType = "web") {
1660
1689
  const grantTypes = resolveRegistrationGrantTypes(client);
1661
1690
  return {
@@ -1681,15 +1710,15 @@ function isNonHttpRedirectUri(redirectUri) {
1681
1710
  /**
1682
1711
  * Server-owned default for dynamic registrations that omit `application_type`.
1683
1712
  * `"infer"` classifies as native when any redirect URI uses a non-http(s)
1684
- * scheme, because a web client can never register one. Every URI is then
1685
- * validated against the native rules. A sent `application_type` is never
1686
- * overridden because `applyOAuthClientRegistrationDefaults` only fills the
1687
- * 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.
1688
1717
  */
1689
1718
  function resolveDynamicRegistrationDefaultApplicationType(opts, client) {
1690
1719
  const configured = opts.clientRegistrationDefaultApplicationType ?? "web";
1691
1720
  if (configured !== "infer") return configured;
1692
- return (client.redirect_uris ?? []).some(isNonHttpRedirectUri) ? "native" : "web";
1721
+ return (client.redirect_uris ?? []).some((uri) => isNonHttpRedirectUri(uri) || isExactLoopbackHttpRedirectUri(uri)) ? "native" : "web";
1693
1722
  }
1694
1723
  function invalidRedirectUri(description) {
1695
1724
  throw new APIError("BAD_REQUEST", {
@@ -1707,6 +1736,10 @@ function getRawHttpHostname(redirectUri) {
1707
1736
  }
1708
1737
  return (hostAndPort.split(":")[0] ?? "").toLowerCase();
1709
1738
  }
1739
+ function isExactLoopbackHttpRedirectUri(redirectUri) {
1740
+ const hostname = getRawHttpHostname(redirectUri);
1741
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
1742
+ }
1710
1743
  function assertDynamicRegistrationResourcesAllowed(opts, requestedResources) {
1711
1744
  const allowedResources = new Set([...opts.clientRegistrationDefaultResources ?? [], ...opts.clientRegistrationAllowedResources ?? []]);
1712
1745
  for (const identifier of requestedResources) if (!allowedResources.has(identifier)) throw new APIError("BAD_REQUEST", {
@@ -1742,8 +1775,6 @@ function validateClientRedirectUri(redirectUri, applicationType) {
1742
1775
  const isHttps = url.protocol === "https:";
1743
1776
  if (/^localhost\.+$/i.test(url.hostname)) invalidRedirectUri(`redirect URI localhost must not include trailing dots: ${redirectUri}`);
1744
1777
  const isRedirectLoopback = isLoopbackIP(url.hostname) || url.hostname === "localhost";
1745
- const rawHttpHostname = getRawHttpHostname(redirectUri);
1746
- const isAllowedNativeHttpLoopback = rawHttpHostname === "localhost" || rawHttpHostname === "127.0.0.1" || rawHttpHostname === "[::1]";
1747
1778
  if (applicationType === "web") {
1748
1779
  if (!isHttps || isRedirectLoopback) invalidRedirectUri(`web clients require https redirect URIs on non-loopback hosts: ${redirectUri}`);
1749
1780
  return;
@@ -1753,18 +1784,11 @@ function validateClientRedirectUri(redirectUri, applicationType) {
1753
1784
  return;
1754
1785
  }
1755
1786
  if (isHttp) {
1756
- 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}`);
1757
1788
  return;
1758
1789
  }
1759
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}`);
1760
1791
  }
1761
- function assertValidRegistrationJwks(jwks) {
1762
- const result = validatePublicClientJwks(jwks);
1763
- if (!result.valid) throw new APIError("BAD_REQUEST", {
1764
- error: "invalid_client_metadata",
1765
- error_description: result.error
1766
- });
1767
- }
1768
1792
  async function registerEndpoint(ctx, opts) {
1769
1793
  const body = ctx.body;
1770
1794
  if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
@@ -1778,8 +1802,15 @@ async function registerEndpoint(ctx, opts) {
1778
1802
  "WWW-Authenticate": "Bearer",
1779
1803
  ...NO_STORE_HEADERS
1780
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
+ };
1781
1812
  if (!session && !isTokenAuthorized) {
1782
- 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", {
1783
1814
  error: "invalid_client_metadata",
1784
1815
  error_description: "client_credentials grant requires authenticated registration"
1785
1816
  });
@@ -1791,7 +1822,7 @@ async function registerEndpoint(ctx, opts) {
1791
1822
  session: session.session
1792
1823
  }) : void 0);
1793
1824
  const response = await createOAuthClientRegistration(ctx, opts, {
1794
- metadata: body,
1825
+ metadata,
1795
1826
  registrationSource: "dynamic",
1796
1827
  userId: referenceId ? void 0 : session?.session.userId,
1797
1828
  referenceId,
@@ -1824,16 +1855,14 @@ async function checkOAuthClient(client, opts, settings) {
1824
1855
  error_description: "Redirect URIs are required for authorization_code and implicit grant types"
1825
1856
  });
1826
1857
  for (const uri of clientWithDefaults.redirect_uris ?? []) validateClientRedirectUri(uri, applicationType ?? (isClientMetadataDocument ? "native" : "web"));
1827
- const supportedGrantTypes = new Set(getSupportedGrantTypes(opts));
1828
- if (isClientMetadataDocument) {
1829
- 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", {
1830
1862
  error: "invalid_client_metadata",
1831
- error_description: `no mutually supported grant_type among ${grantTypes.join(", ")}`
1863
+ error_description: `unsupported grant_type ${grantType}`
1832
1864
  });
1833
- } else for (const grantType of grantTypes) if (!supportedGrantTypes.has(grantType)) throw new APIError("BAD_REQUEST", {
1834
- error: "invalid_client_metadata",
1835
- error_description: `unsupported grant_type ${grantType}`
1836
- });
1865
+ }
1837
1866
  if (grantTypes.includes("authorization_code") && !responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
1838
1867
  error: "invalid_client_metadata",
1839
1868
  error_description: "When 'authorization_code' grant type is used, 'code' response type must be included"
@@ -1913,7 +1942,7 @@ async function checkOAuthClient(client, opts, settings) {
1913
1942
  error_description: "jwks_uri must be a valid URL"
1914
1943
  });
1915
1944
  }
1916
- if (clientWithDefaults.jwks) assertValidRegistrationJwks(clientWithDefaults.jwks);
1945
+ if (clientWithDefaults.jwks) assertValidClientJwks(clientWithDefaults.jwks);
1917
1946
  }
1918
1947
  if (tokenEndpointAuthMethod === "private_key_jwt" && !clientWithDefaults.jwks && !clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
1919
1948
  error: "invalid_client_metadata",
@@ -2418,10 +2447,7 @@ async function deleteClientEndpoint(ctx, opts) {
2418
2447
  await assertClientPrivileges(ctx, session, opts, "delete", ctx.body.client_id);
2419
2448
  if (!session) throw new APIError("UNAUTHORIZED");
2420
2449
  const clientId = ctx.body.client_id;
2421
- if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
2422
- error_description: "trusted clients must be updated manually",
2423
- error: "invalid_client"
2424
- });
2450
+ assertNotTrustedClient(opts, clientId);
2425
2451
  const client = await getClient(ctx, opts, clientId);
2426
2452
  if (!client) throw new APIError("NOT_FOUND", {
2427
2453
  error_description: "client not found",
@@ -2445,10 +2471,7 @@ async function updateClientEndpoint(ctx, opts, settings) {
2445
2471
  await assertClientPrivileges(ctx, session, opts, "update", ctx.body.client_id);
2446
2472
  if (!session) throw new APIError("UNAUTHORIZED");
2447
2473
  const clientId = ctx.body.client_id;
2448
- if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
2449
- error_description: "trusted clients must be updated manually",
2450
- error: "invalid_client"
2451
- });
2474
+ assertNotTrustedClient(opts, clientId);
2452
2475
  const client = await getClient(ctx, opts, clientId);
2453
2476
  if (!client) throw new APIError("NOT_FOUND", {
2454
2477
  error_description: "client not found",
@@ -2516,10 +2539,7 @@ async function rotateClientSecretEndpoint(ctx, opts) {
2516
2539
  await assertClientPrivileges(ctx, session, opts, "rotate", ctx.body.client_id);
2517
2540
  if (!session) throw new APIError("UNAUTHORIZED");
2518
2541
  const clientId = ctx.body.client_id;
2519
- if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
2520
- error_description: "trusted clients must be updated manually",
2521
- error: "invalid_client"
2522
- });
2542
+ assertNotTrustedClient(opts, clientId);
2523
2543
  const client = await getClient(ctx, opts, clientId);
2524
2544
  if (!client) throw new APIError("NOT_FOUND", {
2525
2545
  error_description: "client not found",
@@ -2556,6 +2576,57 @@ async function rotateClientSecretEndpoint(ctx, opts) {
2556
2576
  clientSecret: (opts.prefix?.clientSecret ?? "") + clientSecret
2557
2577
  });
2558
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
+ }
2559
2630
  //#endregion
2560
2631
  //#region src/oauthClient/index.ts
2561
2632
  const tokenEndpointAuthMethodSchema = z.string().trim().min(1);
@@ -3000,6 +3071,29 @@ const rotateClientSecret = (opts) => createAuthEndpoint("/oauth2/client/rotate-s
3000
3071
  }, async (ctx) => {
3001
3072
  return rotateClientSecretEndpoint(ctx, opts);
3002
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
+ });
3003
3097
  const deleteOAuthClient = (opts) => createAuthEndpoint("/oauth2/delete-client", {
3004
3098
  method: "POST",
3005
3099
  use: [sessionMiddleware],
@@ -3465,29 +3559,30 @@ const adminUnlinkClientResource = (opts) => createAuthEndpoint("/admin/oauth2/re
3465
3559
  * introspection and userinfo.
3466
3560
  */
3467
3561
  async function revokeJwtAccessToken(ctx, opts, token) {
3468
- const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
3469
- 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;
3470
3569
  try {
3471
- const verified = await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
3570
+ jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
3472
3571
  jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
3473
- return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
3572
+ return (await jwtPlugin.endpoints.getJwks(ctx))?.response;
3474
3573
  },
3475
3574
  jwksCacheKey: jwtPlugin
3476
- })), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL });
3477
- const userInfoAudience = `${ctx.context.baseURL}/oauth2/userinfo`;
3478
- if (!verified.payload.azp || !await isAudienceClaimAllowed(ctx, opts, verified.payload.aud, [userInfoAudience])) return null;
3575
+ })), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL })).payload;
3479
3576
  } catch (error) {
3480
- if (error instanceof Error) {
3481
- if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
3482
- error_description: "invalid JWT signature",
3483
- error: "invalid_request"
3484
- });
3485
- else if (error.name === "JWTExpired") return null;
3486
- else if (error.name === "JWTInvalid") return null;
3487
- throw error;
3488
- }
3489
- 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;
3490
3583
  }
3584
+ const userInfoAudience = `${ctx.context.baseURL}/oauth2/userinfo`;
3585
+ if (!jwtPayload.azp || !await isAudienceClaimAllowed(ctx, opts, jwtPayload.aud, [userInfoAudience])) return null;
3491
3586
  throw new APIError$1("BAD_REQUEST", {
3492
3587
  error_description: "JWT access tokens are self-contained and cannot be revoked server-side",
3493
3588
  error: "unsupported_token_type"
@@ -4440,6 +4535,10 @@ const oauthProvider = (options) => {
4440
4535
  onRequest: handleIssuerMetadataRequest,
4441
4536
  init: async (ctx) => {
4442
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
+ }
4443
4542
  await seedResourcesOnce(ctx, opts).catch((error) => logStartupSeedFailure(ctx, error));
4444
4543
  logEnforcePerClientResourcesResolution(opts);
4445
4544
  if (!opts.disableJwtPlugin) {
@@ -4593,7 +4692,7 @@ const oauthProvider = (options) => {
4593
4692
  body: z.object({
4594
4693
  selected: z.boolean().optional().meta({ description: "Confirms an account has been selected and authorization can proceed." }),
4595
4694
  created: z.boolean().optional().meta({ description: "Confirms an account was registered" }),
4596
- postLogin: z.boolean().optional().meta({ description: "Confirms organization and/or team selection." }),
4695
+ postLogin: z.boolean().optional().meta({ description: "Resumes after the post login page. postLogin.shouldRedirect runs again with isContinuation: true." }),
4597
4696
  oauth_query: z.string().optional().meta({ description: "The redirected page's query parameters" })
4598
4697
  }),
4599
4698
  use: [sessionMiddleware],
@@ -5254,6 +5353,7 @@ const oauthProvider = (options) => {
5254
5353
  adminUpdateOAuthClient: adminUpdateOAuthClient(opts),
5255
5354
  updateOAuthClient: updateOAuthClient(opts),
5256
5355
  rotateClientSecret: rotateClientSecret(opts),
5356
+ rotateClientKeys: rotateClientKeys(opts),
5257
5357
  deleteOAuthClient: deleteOAuthClient(opts),
5258
5358
  getOAuthConsent: getOAuthConsent(opts),
5259
5359
  getOAuthConsents: getOAuthConsents(opts),
@@ -5294,6 +5394,11 @@ const oauthProvider = (options) => {
5294
5394
  window: opts.rateLimit?.register?.window ?? 60,
5295
5395
  max: opts.rateLimit?.register?.max ?? 5
5296
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
+ }] : [],
5297
5402
  ...opts.rateLimit?.userinfo !== false ? [{
5298
5403
  pathMatcher: (path) => path === "/oauth2/userinfo",
5299
5404
  window: opts.rateLimit?.userinfo?.window ?? 60,
@@ -5603,7 +5708,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
5603
5708
  if (promptSet?.has("select_account") && !opts.selectAccount?.page) return handleRedirect(ctx, getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
5604
5709
  if (!(query.response_type === "code")) return handleRedirect(ctx, getErrorURL(ctx, "unsupported_response_type", "unsupported response type"));
5605
5710
  const client = await getClient(ctx, opts, query.client_id);
5606
- 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"));
5607
5712
  if (client.disabled) return handleRedirect(ctx, getErrorURL(ctx, "client_disabled", "client is disabled"));
5608
5713
  if (!clientAllowsGrant(client, "authorization_code")) return handleRedirect(ctx, getErrorURL(ctx, "unauthorized_client", "client is not authorized to use the authorization_code grant"));
5609
5714
  const registeredUris = client.redirectUris ?? [];
@@ -5701,14 +5806,15 @@ async function authorizeEndpoint(ctx, opts, settings) {
5701
5806
  headers: request.headers,
5702
5807
  user: session.user,
5703
5808
  session: session.session,
5704
- scopes: requestedScopes
5809
+ scopes: requestedScopes,
5810
+ isContinuation: settings?.isPostLoginContinuation ?? false
5705
5811
  })) {
5706
5812
  if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "interaction_required", "End-User interaction is required");
5707
5813
  return redirectWithPromptCode(ctx, opts, "post_login");
5708
5814
  }
5709
5815
  }
5710
5816
  if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
5711
- const referenceId = await opts.postLogin?.consentReferenceId?.({
5817
+ const referenceId = await getConsentReferenceId(opts, {
5712
5818
  user: session.user,
5713
5819
  session: session.session,
5714
5820
  scopes: requestedScopes
@@ -5722,23 +5828,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
5722
5828
  referenceId,
5723
5829
  resource: requestedResources
5724
5830
  });
5725
- const consent = await ctx.context.adapter.findOne({
5726
- model: "oauthConsent",
5727
- where: [
5728
- {
5729
- field: "clientId",
5730
- value: client.clientId
5731
- },
5732
- {
5733
- field: "userId",
5734
- value: session.user.id
5735
- },
5736
- ...referenceId ? [{
5737
- field: "referenceId",
5738
- value: referenceId
5739
- }] : []
5740
- ]
5741
- });
5831
+ const consent = await findConsent(ctx, client.clientId, session.user.id, referenceId);
5742
5832
  if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val)) || !requestedUserInfoClaims.every((claim) => (consent.requestedUserInfoClaims ?? []).includes(claim))) {
5743
5833
  if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "consent_required", "End-User consent is required");
5744
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-C_RnL2nt.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-CGIZS-Da.mjs";
2
- import { t as PACKAGE_VERSION } from "./version-BPS0os17.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-pKAoqtlo.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-BPS0os17.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-C_RnL2nt.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-pKAoqtlo.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-H3uoUiuS.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-CGIZS-Da.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-C95F-9E1.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-C_RnL2nt.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-C95F-9E1.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-CGIZS-Da.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
@@ -1083,8 +1083,22 @@ function logEnforcePerClientResourcesResolution(opts) {
1083
1083
  function getDpopProofJwt(ctx) {
1084
1084
  return ctx.headers?.get("dpop") ?? void 0;
1085
1085
  }
1086
+ /**
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.
1092
+ */
1086
1093
  function getEndpointUrl(ctx, path) {
1087
- return ctx.request?.url ?? `${ctx.context.baseURL}${path}`;
1094
+ const { baseURL } = ctx.context;
1095
+ let end = baseURL.length;
1096
+ while (end > 0 && baseURL[end - 1] === "/") end--;
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}`;
1088
1102
  }
1089
1103
  //#endregion
1090
1104
  //#region src/oauthClient/client-credentials.ts
@@ -1638,7 +1652,7 @@ async function resolveDpopTokenBinding(ctx, opts, params) {
1638
1652
  return { jkt: (await verifyDpopProof({
1639
1653
  proofJwt: dpopProofJwt,
1640
1654
  method: "POST",
1641
- url: getEndpointUrl(ctx, "/oauth2/token"),
1655
+ url: getEndpointUrl(ctx, ctx.path ?? "/oauth2/token"),
1642
1656
  expectedJkt,
1643
1657
  proofMaxAgeSeconds: opts.dpop?.proofMaxAgeSeconds,
1644
1658
  signingAlgorithms: opts.dpop?.signingAlgorithms,
@@ -1964,6 +1978,10 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1964
1978
  error_description: "Either code_verifier or client_secret is required",
1965
1979
  error: "invalid_request"
1966
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);
1967
1985
  /** Get and check Verification Value */
1968
1986
  const { verificationValue, effectiveResources, authorizedResources, authorizationCodeId } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
1969
1987
  const scopes = verificationValue.query.scope?.split(" ");
@@ -1971,8 +1989,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1971
1989
  error_description: "verification scope unset",
1972
1990
  error: "invalid_scope"
1973
1991
  });
1974
- /** Verify Client */
1975
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerified, "authorization_code", authMethod);
1992
+ validateClientScopes(client, scopes);
1976
1993
  if (isPKCERequired(client, {
1977
1994
  scopes: (verificationValue.query?.scope)?.split(" ") || [],
1978
1995
  nonce: verificationValue.query?.nonce
@@ -1989,17 +2006,17 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
1989
2006
  const pkceUsedInAuth = !!verificationValue.query?.code_challenge;
1990
2007
  const pkceUsedInToken = !!code_verifier;
1991
2008
  if (pkceUsedInAuth || pkceUsedInToken) {
1992
- if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
2009
+ if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("BAD_REQUEST", {
1993
2010
  error_description: "code_verifier required because PKCE was used in authorization",
1994
2011
  error: "invalid_request"
1995
2012
  });
1996
- if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
2013
+ if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("BAD_REQUEST", {
1997
2014
  error_description: "code_verifier provided but PKCE was not used in authorization",
1998
2015
  error: "invalid_request"
1999
2016
  });
2000
- 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", {
2001
2018
  error_description: "code verification failed",
2002
- error: "invalid_request"
2019
+ error: "invalid_grant"
2003
2020
  });
2004
2021
  }
2005
2022
  /** Get user */
@@ -2102,6 +2119,10 @@ async function handleRefreshTokenGrant(ctx, opts) {
2102
2119
  error_description: "Missing a required refresh_token for refresh_token grant",
2103
2120
  error: "invalid_request"
2104
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);
2105
2126
  const decodedRefresh = await decodeRefreshToken(opts, refresh_token);
2106
2127
  const refreshToken = await ctx.context.adapter.findOne({
2107
2128
  model: "oauthRefreshToken",
@@ -2135,7 +2156,7 @@ async function handleRefreshTokenGrant(ctx, opts) {
2135
2156
  error: "invalid_scope"
2136
2157
  });
2137
2158
  }
2138
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes, preVerified, "refresh_token", authMethod);
2159
+ validateClientScopes(client, requestedScopes ?? scopes);
2139
2160
  if (refreshToken.revoked) {
2140
2161
  if (isWithinRefreshTokenReuseInterval(refreshToken)) {
2141
2162
  const replay = await getRefreshTokenRotationReplay(ctx, refreshToken, await resolveRefreshTokenRotationReplayRequest(ctx, opts, {
@@ -2228,29 +2249,29 @@ async function isIntrospectionAuthorized(ctx, opts, params) {
2228
2249
  * @returns RFC7662 introspection format
2229
2250
  */
2230
2251
  async function validateJwtAccessToken(ctx, opts, token, clientId) {
2231
- const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
2232
- 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;
2233
2258
  const userInfoAud = `${ctx.context.baseURL ?? ""}/oauth2/userinfo`;
2234
2259
  const expectedIssuer = jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL;
2235
2260
  let jwtPayload;
2236
2261
  try {
2237
2262
  jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
2238
2263
  jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
2239
- return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
2264
+ return (await jwtPlugin.endpoints.getJwks(ctx))?.response;
2240
2265
  },
2241
2266
  jwksCacheKey: jwtPlugin
2242
2267
  })), { issuer: expectedIssuer })).payload;
2243
2268
  } catch (error) {
2244
- if (error instanceof Error) {
2245
- if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
2246
- error_description: "invalid JWT signature",
2247
- error: "invalid_request"
2248
- });
2249
- else if (error.name === "JWTExpired") return { active: false };
2250
- else if (error.name === "JWTInvalid") return { active: false };
2251
- throw error;
2252
- }
2253
- 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;
2254
2275
  }
2255
2276
  const rawAud = jwtPayload.aud;
2256
2277
  if (!await isAudienceClaimAllowed(ctx, opts, rawAud, [userInfoAud])) return { active: false };
@@ -2541,4 +2562,4 @@ async function introspectEndpoint(ctx, opts) {
2541
2562
  }
2542
2563
  }
2543
2564
  //#endregion
2544
- 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-C_RnL2nt.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.
@@ -1642,7 +1660,8 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
1642
1660
  * For example, allow selection of an organization or team.
1643
1661
  *
1644
1662
  * Upon selection of a specific account, use `/oauth2/continue`
1645
- * with `postLogin: true` to continue the login flow.
1663
+ * with `postLogin: true` to continue the login flow. The continue
1664
+ * request runs `shouldRedirect` again with `isContinuation: true`.
1646
1665
  *
1647
1666
  * @returns
1648
1667
  * - `true`: account is not selected and needs selection
@@ -1653,6 +1672,12 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
1653
1672
  user: User & Record<string, unknown>;
1654
1673
  session: Session & Record<string, unknown>;
1655
1674
  scopes: Scopes;
1675
+ /**
1676
+ * `true` when the user returns from `page` through `/oauth2/continue`.
1677
+ * The client chooses to send that request, so it does not prove a
1678
+ * selection was made: validate the session before returning `false`.
1679
+ */
1680
+ isContinuation: boolean;
1656
1681
  }) => Awaitable<boolean>;
1657
1682
  };
1658
1683
  /**
@@ -1930,6 +1955,7 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
1930
1955
  * introspect: { window: 60, max: 100 },
1931
1956
  * revoke: { window: 60, max: 30 },
1932
1957
  * register: { window: 60, max: 5 },
1958
+ * rotateClientKeys: { window: 60, max: 5 },
1933
1959
  * userinfo: { window: 60, max: 60 },
1934
1960
  * }
1935
1961
  * ```
@@ -1975,6 +2001,14 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
1975
2001
  window: number;
1976
2002
  max: number;
1977
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;
1978
2012
  /**
1979
2013
  * Rate limit for /oauth2/userinfo endpoint
1980
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-C95F-9E1.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.8";
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.8",
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.8",
75
- "@better-auth/memory-adapter": "1.7.8",
76
- "better-auth": "1.7.8"
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.8",
83
- "better-auth": "^1.7.8"
82
+ "@better-auth/core": "^1.7.10",
83
+ "better-auth": "^1.7.10"
84
84
  },
85
85
  "scripts": {
86
86
  "build": "tsdown",