@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.
- package/dist/{authorize-C95F-9E1.mjs → authorize-dkOG8GqZ.mjs} +193 -103
- package/dist/client-jwks-DY2QBrsl.d.mts +25 -0
- package/dist/client-resource.d.mts +2 -2
- package/dist/client-resource.mjs +2 -2
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +6 -5
- package/dist/internal.d.mts +2 -21
- package/dist/internal.mjs +1 -1
- package/dist/{introspect-H3uoUiuS.mjs → introspect-BVm_zFQC.mjs} +46 -25
- package/dist/{oauth-pKAoqtlo.d.mts → oauth-Cse_eZ6i.d.mts} +30 -1
- package/dist/{oauth-C_RnL2nt.d.mts → oauth-DXiqbRzK.d.mts} +42 -8
- package/dist/{utils-CGIZS-Da.mjs → utils-DfMNRh3J.mjs} +74 -3
- package/dist/{version-BPS0os17.mjs → version-aKfgWHzo.mjs} +1 -1
- package/package.json +6 -6
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
-
import { A as
|
|
3
|
-
import {
|
|
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-
|
|
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
|
|
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.
|
|
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, {
|
|
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: "
|
|
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 {
|
|
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
|
|
1685
|
-
* validated against the native rules. A
|
|
1686
|
-
*
|
|
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 (!
|
|
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 (
|
|
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
|
|
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
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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: `
|
|
1863
|
+
error_description: `unsupported grant_type ${grantType}`
|
|
1832
1864
|
});
|
|
1833
|
-
}
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3469
|
-
|
|
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
|
-
|
|
3570
|
+
jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
|
|
3472
3571
|
jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
|
|
3473
|
-
return (await jwtPlugin
|
|
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
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
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: "
|
|
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", "
|
|
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
|
|
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.
|
|
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-
|
|
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
|
package/dist/client-resource.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
2
|
-
import {
|
|
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-
|
|
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
|
|
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
|
}
|
package/dist/internal.d.mts
CHANGED
|
@@ -1,26 +1,7 @@
|
|
|
1
|
-
import { H as Scope, V as SchemaClient, j as OAuthOptions, o as OAuthClient } from "./oauth-
|
|
2
|
-
import {
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
2232
|
-
|
|
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
|
|
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
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
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 {
|
|
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-
|
|
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
|
|
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
|
|
1441
|
-
*
|
|
1442
|
-
*
|
|
1443
|
-
*
|
|
1444
|
-
*
|
|
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("
|
|
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-
|
|
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 {
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/oauth-provider",
|
|
3
|
-
"version": "1.7.
|
|
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.
|
|
75
|
-
"@better-auth/memory-adapter": "1.7.
|
|
76
|
-
"better-auth": "1.7.
|
|
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.
|
|
83
|
-
"better-auth": "^1.7.
|
|
82
|
+
"@better-auth/core": "^1.7.10",
|
|
83
|
+
"better-auth": "^1.7.10"
|
|
84
84
|
},
|
|
85
85
|
"scripts": {
|
|
86
86
|
"build": "tsdown",
|