@better-auth/oauth-provider 1.7.9 → 1.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{authorize-D8r6mvZx.mjs → authorize-dkOG8GqZ.mjs} +186 -100
- 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-7WCMPe5Z.mjs → introspect-BVm_zFQC.mjs} +40 -26
- package/dist/{oauth-BWgg8X-A.d.mts → oauth-Cse_eZ6i.d.mts} +30 -1
- package/dist/{oauth-DAMtT2vg.d.mts → oauth-DXiqbRzK.d.mts} +34 -7
- package/dist/{utils-DKU5wblX.mjs → utils-DfMNRh3J.mjs} +74 -3
- package/dist/{version-DVUrz-nv.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 = {
|
|
@@ -1032,6 +1016,17 @@ function validatePublicClientJwks(input) {
|
|
|
1032
1016
|
jwks: { keys }
|
|
1033
1017
|
};
|
|
1034
1018
|
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Throws `invalid_client_metadata` unless `jwks` is a public JWK Set that
|
|
1021
|
+
* {@link validatePublicClientJwks} accepts.
|
|
1022
|
+
*/
|
|
1023
|
+
function assertValidClientJwks(jwks) {
|
|
1024
|
+
const result = validatePublicClientJwks(jwks);
|
|
1025
|
+
if (!result.valid) throw new APIError("BAD_REQUEST", {
|
|
1026
|
+
error: "invalid_client_metadata",
|
|
1027
|
+
error_description: result.error
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1035
1030
|
//#endregion
|
|
1036
1031
|
//#region src/client-metadata.ts
|
|
1037
1032
|
const OAUTH_CLIENT_RECORD_FIELD_NAMES = Object.freeze(Object.keys({
|
|
@@ -1492,8 +1487,20 @@ async function consumeClientAssertion(ctx, opts, params) {
|
|
|
1492
1487
|
* Validates: signature, iss=client_id, sub=client_id, aud=the endpoint serving
|
|
1493
1488
|
* the assertion or the normalized OP issuer, exp, assertion max lifetime,
|
|
1494
1489
|
* jti uniqueness (replay prevention).
|
|
1490
|
+
*
|
|
1491
|
+
* Returns the client record the signature was checked against, so a caller that
|
|
1492
|
+
* mutates the client can guard its write on exactly that snapshot.
|
|
1493
|
+
*
|
|
1494
|
+
* With `endpointAudienceOnly`, the issuer is not an accepted audience, so only
|
|
1495
|
+
* an assertion whose `aud` includes `expectedAudience` verifies. Endpoints that
|
|
1496
|
+
* change client credentials use it, so an assertion minted for the token
|
|
1497
|
+
* endpoint or the issuer cannot be redirected to them.
|
|
1498
|
+
*
|
|
1499
|
+
* `assertClient` runs on the looked-up client before any key is fetched or the
|
|
1500
|
+
* jti is consumed, so a caller can refuse a client it does not serve without
|
|
1501
|
+
* spending the assertion or reaching out to a `jwks_uri`.
|
|
1495
1502
|
*/
|
|
1496
|
-
async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertionType, clientIdHint, expectedAudience) {
|
|
1503
|
+
async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertionType, clientIdHint, expectedAudience, options) {
|
|
1497
1504
|
if (clientAssertionType !== CLIENT_ASSERTION_TYPE) throw new APIError$1("BAD_REQUEST", {
|
|
1498
1505
|
error_description: "unsupported client_assertion_type",
|
|
1499
1506
|
error: "invalid_client"
|
|
@@ -1531,7 +1538,7 @@ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertion
|
|
|
1531
1538
|
});
|
|
1532
1539
|
const client = await getClient(ctx, opts, clientId);
|
|
1533
1540
|
if (!client) throw new APIError$1("BAD_REQUEST", {
|
|
1534
|
-
error_description: "
|
|
1541
|
+
error_description: "client not found",
|
|
1535
1542
|
error: "invalid_client"
|
|
1536
1543
|
});
|
|
1537
1544
|
if (client.disabled) throw new APIError$1("BAD_REQUEST", {
|
|
@@ -1542,10 +1549,11 @@ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertion
|
|
|
1542
1549
|
error_description: "client is not registered for private_key_jwt authentication",
|
|
1543
1550
|
error: "invalid_client"
|
|
1544
1551
|
});
|
|
1552
|
+
options?.assertClient?.(client);
|
|
1545
1553
|
const jwks = await fetchClientJwks(ctx, opts, client);
|
|
1546
1554
|
const endpointAudience = expectedAudience ?? `${ctx.context.baseURL}/oauth2/token`;
|
|
1547
1555
|
const issuerAudience = getIssuer(ctx, opts);
|
|
1548
|
-
const acceptedAudiences = [...new Set([endpointAudience, issuerAudience])];
|
|
1556
|
+
const acceptedAudiences = options?.endpointAudienceOnly ? [endpointAudience] : [...new Set([endpointAudience, issuerAudience])];
|
|
1549
1557
|
const verifyOpts = {
|
|
1550
1558
|
issuer: clientId,
|
|
1551
1559
|
subject: clientId,
|
|
@@ -1581,7 +1589,10 @@ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertion
|
|
|
1581
1589
|
payload,
|
|
1582
1590
|
expectedAudience: acceptedAudience
|
|
1583
1591
|
});
|
|
1584
|
-
return {
|
|
1592
|
+
return {
|
|
1593
|
+
clientId,
|
|
1594
|
+
client
|
|
1595
|
+
};
|
|
1585
1596
|
}
|
|
1586
1597
|
//#endregion
|
|
1587
1598
|
//#region src/utils/initial-access-token.ts
|
|
@@ -1659,6 +1670,21 @@ function resolveRegistrationResponseTypes(client, grantTypes) {
|
|
|
1659
1670
|
if (client.response_types) return client.response_types;
|
|
1660
1671
|
return grantTypes.includes("authorization_code") ? ["code"] : void 0;
|
|
1661
1672
|
}
|
|
1673
|
+
/**
|
|
1674
|
+
* The requested grants this server supports. Clients such as MCP connectors
|
|
1675
|
+
* send one grant union to every server, so a metadata document or a dynamic
|
|
1676
|
+
* registration may list grants this server does not issue. Throws when the
|
|
1677
|
+
* request shares no grant with the server.
|
|
1678
|
+
*/
|
|
1679
|
+
function resolveMutuallySupportedGrantTypes(opts, grantTypes) {
|
|
1680
|
+
const supportedGrantTypes = new Set(getSupportedGrantTypes(opts));
|
|
1681
|
+
const mutualGrantTypes = grantTypes.filter((grantType) => supportedGrantTypes.has(grantType));
|
|
1682
|
+
if (mutualGrantTypes.length === 0) throw new APIError("BAD_REQUEST", {
|
|
1683
|
+
error: "invalid_client_metadata",
|
|
1684
|
+
error_description: `no mutually supported grant_type among ${grantTypes.join(", ")}`
|
|
1685
|
+
});
|
|
1686
|
+
return mutualGrantTypes;
|
|
1687
|
+
}
|
|
1662
1688
|
function applyOAuthClientRegistrationDefaults(client, defaultApplicationType = "web") {
|
|
1663
1689
|
const grantTypes = resolveRegistrationGrantTypes(client);
|
|
1664
1690
|
return {
|
|
@@ -1684,15 +1710,15 @@ function isNonHttpRedirectUri(redirectUri) {
|
|
|
1684
1710
|
/**
|
|
1685
1711
|
* Server-owned default for dynamic registrations that omit `application_type`.
|
|
1686
1712
|
* `"infer"` classifies as native when any redirect URI uses a non-http(s)
|
|
1687
|
-
* scheme, because a web client can never
|
|
1688
|
-
* validated against the native rules. A
|
|
1689
|
-
*
|
|
1690
|
-
* absent field.
|
|
1713
|
+
* scheme or http on an exact loopback host, because a web client can never
|
|
1714
|
+
* register either. Every URI is then validated against the native rules. A
|
|
1715
|
+
* sent `application_type` is never overridden because
|
|
1716
|
+
* `applyOAuthClientRegistrationDefaults` only fills the absent field.
|
|
1691
1717
|
*/
|
|
1692
1718
|
function resolveDynamicRegistrationDefaultApplicationType(opts, client) {
|
|
1693
1719
|
const configured = opts.clientRegistrationDefaultApplicationType ?? "web";
|
|
1694
1720
|
if (configured !== "infer") return configured;
|
|
1695
|
-
return (client.redirect_uris ?? []).some(isNonHttpRedirectUri) ? "native" : "web";
|
|
1721
|
+
return (client.redirect_uris ?? []).some((uri) => isNonHttpRedirectUri(uri) || isExactLoopbackHttpRedirectUri(uri)) ? "native" : "web";
|
|
1696
1722
|
}
|
|
1697
1723
|
function invalidRedirectUri(description) {
|
|
1698
1724
|
throw new APIError("BAD_REQUEST", {
|
|
@@ -1710,6 +1736,10 @@ function getRawHttpHostname(redirectUri) {
|
|
|
1710
1736
|
}
|
|
1711
1737
|
return (hostAndPort.split(":")[0] ?? "").toLowerCase();
|
|
1712
1738
|
}
|
|
1739
|
+
function isExactLoopbackHttpRedirectUri(redirectUri) {
|
|
1740
|
+
const hostname = getRawHttpHostname(redirectUri);
|
|
1741
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
1742
|
+
}
|
|
1713
1743
|
function assertDynamicRegistrationResourcesAllowed(opts, requestedResources) {
|
|
1714
1744
|
const allowedResources = new Set([...opts.clientRegistrationDefaultResources ?? [], ...opts.clientRegistrationAllowedResources ?? []]);
|
|
1715
1745
|
for (const identifier of requestedResources) if (!allowedResources.has(identifier)) throw new APIError("BAD_REQUEST", {
|
|
@@ -1745,8 +1775,6 @@ function validateClientRedirectUri(redirectUri, applicationType) {
|
|
|
1745
1775
|
const isHttps = url.protocol === "https:";
|
|
1746
1776
|
if (/^localhost\.+$/i.test(url.hostname)) invalidRedirectUri(`redirect URI localhost must not include trailing dots: ${redirectUri}`);
|
|
1747
1777
|
const isRedirectLoopback = isLoopbackIP(url.hostname) || url.hostname === "localhost";
|
|
1748
|
-
const rawHttpHostname = getRawHttpHostname(redirectUri);
|
|
1749
|
-
const isAllowedNativeHttpLoopback = rawHttpHostname === "localhost" || rawHttpHostname === "127.0.0.1" || rawHttpHostname === "[::1]";
|
|
1750
1778
|
if (applicationType === "web") {
|
|
1751
1779
|
if (!isHttps || isRedirectLoopback) invalidRedirectUri(`web clients require https redirect URIs on non-loopback hosts: ${redirectUri}`);
|
|
1752
1780
|
return;
|
|
@@ -1756,18 +1784,11 @@ function validateClientRedirectUri(redirectUri, applicationType) {
|
|
|
1756
1784
|
return;
|
|
1757
1785
|
}
|
|
1758
1786
|
if (isHttp) {
|
|
1759
|
-
if (!
|
|
1787
|
+
if (!isExactLoopbackHttpRedirectUri(redirectUri)) invalidRedirectUri(`native clients may use http only on the exact loopback hosts localhost, 127.0.0.1, or [::1]: ${redirectUri}`);
|
|
1760
1788
|
return;
|
|
1761
1789
|
}
|
|
1762
1790
|
if (!isNativePrivateUseRedirectUri(url)) invalidRedirectUri(`native private-use redirect URI schemes must not use a reserved scheme; they must be an authority-free reverse-domain URI or a custom-scheme URI with a host and a path: ${redirectUri}`);
|
|
1763
1791
|
}
|
|
1764
|
-
function assertValidRegistrationJwks(jwks) {
|
|
1765
|
-
const result = validatePublicClientJwks(jwks);
|
|
1766
|
-
if (!result.valid) throw new APIError("BAD_REQUEST", {
|
|
1767
|
-
error: "invalid_client_metadata",
|
|
1768
|
-
error_description: result.error
|
|
1769
|
-
});
|
|
1770
|
-
}
|
|
1771
1792
|
async function registerEndpoint(ctx, opts) {
|
|
1772
1793
|
const body = ctx.body;
|
|
1773
1794
|
if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
|
|
@@ -1781,8 +1802,15 @@ async function registerEndpoint(ctx, opts) {
|
|
|
1781
1802
|
"WWW-Authenticate": "Bearer",
|
|
1782
1803
|
...NO_STORE_HEADERS
|
|
1783
1804
|
});
|
|
1805
|
+
const grantTypes = body.grant_types?.length ? resolveMutuallySupportedGrantTypes(opts, body.grant_types) : body.grant_types;
|
|
1806
|
+
const droppedAuthorizationCode = body.grant_types?.includes("authorization_code") && !grantTypes?.includes("authorization_code");
|
|
1807
|
+
const metadata = {
|
|
1808
|
+
...body,
|
|
1809
|
+
grant_types: grantTypes,
|
|
1810
|
+
response_types: droppedAuthorizationCode ? void 0 : body.response_types
|
|
1811
|
+
};
|
|
1784
1812
|
if (!session && !isTokenAuthorized) {
|
|
1785
|
-
if (
|
|
1813
|
+
if (metadata.grant_types?.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
|
|
1786
1814
|
error: "invalid_client_metadata",
|
|
1787
1815
|
error_description: "client_credentials grant requires authenticated registration"
|
|
1788
1816
|
});
|
|
@@ -1794,7 +1822,7 @@ async function registerEndpoint(ctx, opts) {
|
|
|
1794
1822
|
session: session.session
|
|
1795
1823
|
}) : void 0);
|
|
1796
1824
|
const response = await createOAuthClientRegistration(ctx, opts, {
|
|
1797
|
-
metadata
|
|
1825
|
+
metadata,
|
|
1798
1826
|
registrationSource: "dynamic",
|
|
1799
1827
|
userId: referenceId ? void 0 : session?.session.userId,
|
|
1800
1828
|
referenceId,
|
|
@@ -1827,16 +1855,14 @@ async function checkOAuthClient(client, opts, settings) {
|
|
|
1827
1855
|
error_description: "Redirect URIs are required for authorization_code and implicit grant types"
|
|
1828
1856
|
});
|
|
1829
1857
|
for (const uri of clientWithDefaults.redirect_uris ?? []) validateClientRedirectUri(uri, applicationType ?? (isClientMetadataDocument ? "native" : "web"));
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1858
|
+
if (isClientMetadataDocument) resolveMutuallySupportedGrantTypes(opts, grantTypes);
|
|
1859
|
+
else {
|
|
1860
|
+
const supportedGrantTypes = new Set(getSupportedGrantTypes(opts));
|
|
1861
|
+
for (const grantType of grantTypes) if (!supportedGrantTypes.has(grantType)) throw new APIError("BAD_REQUEST", {
|
|
1833
1862
|
error: "invalid_client_metadata",
|
|
1834
|
-
error_description: `
|
|
1863
|
+
error_description: `unsupported grant_type ${grantType}`
|
|
1835
1864
|
});
|
|
1836
|
-
}
|
|
1837
|
-
error: "invalid_client_metadata",
|
|
1838
|
-
error_description: `unsupported grant_type ${grantType}`
|
|
1839
|
-
});
|
|
1865
|
+
}
|
|
1840
1866
|
if (grantTypes.includes("authorization_code") && !responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
|
|
1841
1867
|
error: "invalid_client_metadata",
|
|
1842
1868
|
error_description: "When 'authorization_code' grant type is used, 'code' response type must be included"
|
|
@@ -1916,7 +1942,7 @@ async function checkOAuthClient(client, opts, settings) {
|
|
|
1916
1942
|
error_description: "jwks_uri must be a valid URL"
|
|
1917
1943
|
});
|
|
1918
1944
|
}
|
|
1919
|
-
if (clientWithDefaults.jwks)
|
|
1945
|
+
if (clientWithDefaults.jwks) assertValidClientJwks(clientWithDefaults.jwks);
|
|
1920
1946
|
}
|
|
1921
1947
|
if (tokenEndpointAuthMethod === "private_key_jwt" && !clientWithDefaults.jwks && !clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
|
|
1922
1948
|
error: "invalid_client_metadata",
|
|
@@ -2421,10 +2447,7 @@ async function deleteClientEndpoint(ctx, opts) {
|
|
|
2421
2447
|
await assertClientPrivileges(ctx, session, opts, "delete", ctx.body.client_id);
|
|
2422
2448
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
2423
2449
|
const clientId = ctx.body.client_id;
|
|
2424
|
-
|
|
2425
|
-
error_description: "trusted clients must be updated manually",
|
|
2426
|
-
error: "invalid_client"
|
|
2427
|
-
});
|
|
2450
|
+
assertNotTrustedClient(opts, clientId);
|
|
2428
2451
|
const client = await getClient(ctx, opts, clientId);
|
|
2429
2452
|
if (!client) throw new APIError("NOT_FOUND", {
|
|
2430
2453
|
error_description: "client not found",
|
|
@@ -2448,10 +2471,7 @@ async function updateClientEndpoint(ctx, opts, settings) {
|
|
|
2448
2471
|
await assertClientPrivileges(ctx, session, opts, "update", ctx.body.client_id);
|
|
2449
2472
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
2450
2473
|
const clientId = ctx.body.client_id;
|
|
2451
|
-
|
|
2452
|
-
error_description: "trusted clients must be updated manually",
|
|
2453
|
-
error: "invalid_client"
|
|
2454
|
-
});
|
|
2474
|
+
assertNotTrustedClient(opts, clientId);
|
|
2455
2475
|
const client = await getClient(ctx, opts, clientId);
|
|
2456
2476
|
if (!client) throw new APIError("NOT_FOUND", {
|
|
2457
2477
|
error_description: "client not found",
|
|
@@ -2519,10 +2539,7 @@ async function rotateClientSecretEndpoint(ctx, opts) {
|
|
|
2519
2539
|
await assertClientPrivileges(ctx, session, opts, "rotate", ctx.body.client_id);
|
|
2520
2540
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
2521
2541
|
const clientId = ctx.body.client_id;
|
|
2522
|
-
|
|
2523
|
-
error_description: "trusted clients must be updated manually",
|
|
2524
|
-
error: "invalid_client"
|
|
2525
|
-
});
|
|
2542
|
+
assertNotTrustedClient(opts, clientId);
|
|
2526
2543
|
const client = await getClient(ctx, opts, clientId);
|
|
2527
2544
|
if (!client) throw new APIError("NOT_FOUND", {
|
|
2528
2545
|
error_description: "client not found",
|
|
@@ -2559,6 +2576,57 @@ async function rotateClientSecretEndpoint(ctx, opts) {
|
|
|
2559
2576
|
clientSecret: (opts.prefix?.clientSecret ?? "") + clientSecret
|
|
2560
2577
|
});
|
|
2561
2578
|
}
|
|
2579
|
+
/**
|
|
2580
|
+
* The write is guarded on the exact key set the assertion was verified against.
|
|
2581
|
+
* A concurrent rotation changes that value, so the stale request loses instead
|
|
2582
|
+
* of overwriting a newer key set.
|
|
2583
|
+
*
|
|
2584
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7523#section-3
|
|
2585
|
+
*/
|
|
2586
|
+
async function rotateClientKeysEndpoint(ctx, opts) {
|
|
2587
|
+
if (!opts.allowClientKeyRotation) throw new APIError("FORBIDDEN", {
|
|
2588
|
+
error: "access_denied",
|
|
2589
|
+
error_description: "Client key rotation is disabled"
|
|
2590
|
+
});
|
|
2591
|
+
const { body } = ctx;
|
|
2592
|
+
assertValidClientJwks(body.jwks);
|
|
2593
|
+
const { client } = await verifyClientAssertion(ctx, opts, body.client_assertion, body.client_assertion_type, void 0, getEndpointUrl({
|
|
2594
|
+
context: ctx.context,
|
|
2595
|
+
request: void 0
|
|
2596
|
+
}, "/oauth2/client/rotate-keys"), {
|
|
2597
|
+
endpointAudienceOnly: true,
|
|
2598
|
+
assertClient: (candidate) => assertRotatable(opts, candidate)
|
|
2599
|
+
});
|
|
2600
|
+
if (!await ctx.context.adapter.incrementOne({
|
|
2601
|
+
model: "oauthClient",
|
|
2602
|
+
where: [{
|
|
2603
|
+
field: "clientId",
|
|
2604
|
+
value: client.clientId
|
|
2605
|
+
}, {
|
|
2606
|
+
field: "jwks",
|
|
2607
|
+
value: client.jwks
|
|
2608
|
+
}],
|
|
2609
|
+
increment: {},
|
|
2610
|
+
set: {
|
|
2611
|
+
jwks: JSON.stringify(body.jwks),
|
|
2612
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2613
|
+
}
|
|
2614
|
+
})) throw new APIError("CONFLICT", {
|
|
2615
|
+
error: "invalid_request",
|
|
2616
|
+
error_description: "client keys changed while rotating; retry with a fresh client assertion"
|
|
2617
|
+
});
|
|
2618
|
+
return {
|
|
2619
|
+
client_id: client.clientId,
|
|
2620
|
+
jwks: body.jwks
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
function assertRotatable(opts, client) {
|
|
2624
|
+
assertNotTrustedClient(opts, client.clientId);
|
|
2625
|
+
if (client.clientDiscoveryId || client.jwksUri || !client.jwks) throw new APIError("BAD_REQUEST", {
|
|
2626
|
+
error: "invalid_client",
|
|
2627
|
+
error_description: "client keys are not stored inline; rotate them where the client publishes them"
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2562
2630
|
//#endregion
|
|
2563
2631
|
//#region src/oauthClient/index.ts
|
|
2564
2632
|
const tokenEndpointAuthMethodSchema = z.string().trim().min(1);
|
|
@@ -3003,6 +3071,29 @@ const rotateClientSecret = (opts) => createAuthEndpoint("/oauth2/client/rotate-s
|
|
|
3003
3071
|
}, async (ctx) => {
|
|
3004
3072
|
return rotateClientSecretEndpoint(ctx, opts);
|
|
3005
3073
|
});
|
|
3074
|
+
/**
|
|
3075
|
+
* Replaces a `private_key_jwt` client's own signing keys. The client proves it
|
|
3076
|
+
* holds a current key with a client assertion, so no session is needed.
|
|
3077
|
+
*/
|
|
3078
|
+
const rotateClientKeys = (opts) => createOAuthEndpoint("/oauth2/client/rotate-keys", {
|
|
3079
|
+
method: "POST",
|
|
3080
|
+
body: z.object({
|
|
3081
|
+
client_assertion: z.string(),
|
|
3082
|
+
client_assertion_type: z.string(),
|
|
3083
|
+
jwks: clientJwksSchema
|
|
3084
|
+
}),
|
|
3085
|
+
errorCodesByField: {
|
|
3086
|
+
client_assertion: "invalid_client",
|
|
3087
|
+
client_assertion_type: "invalid_client",
|
|
3088
|
+
jwks: "invalid_client_metadata"
|
|
3089
|
+
},
|
|
3090
|
+
metadata: {
|
|
3091
|
+
noStore: true,
|
|
3092
|
+
openapi: { description: "Replaces the signing keys of a private_key_jwt client. The request is authenticated by a client assertion signed with one of the client's current keys and addressed to this endpoint. Keys left out of the new set stop verifying at once." }
|
|
3093
|
+
}
|
|
3094
|
+
}, async (ctx) => {
|
|
3095
|
+
return rotateClientKeysEndpoint(ctx, opts);
|
|
3096
|
+
});
|
|
3006
3097
|
const deleteOAuthClient = (opts) => createAuthEndpoint("/oauth2/delete-client", {
|
|
3007
3098
|
method: "POST",
|
|
3008
3099
|
use: [sessionMiddleware],
|
|
@@ -3468,29 +3559,30 @@ const adminUnlinkClientResource = (opts) => createAuthEndpoint("/admin/oauth2/re
|
|
|
3468
3559
|
* introspection and userinfo.
|
|
3469
3560
|
*/
|
|
3470
3561
|
async function revokeJwtAccessToken(ctx, opts, token) {
|
|
3471
|
-
|
|
3472
|
-
|
|
3562
|
+
if (opts.disableJwtPlugin) throw new APIError$1("BAD_REQUEST", {
|
|
3563
|
+
error_description: "JWT access tokens are disabled",
|
|
3564
|
+
error: "invalid_request"
|
|
3565
|
+
});
|
|
3566
|
+
const jwtPlugin = getJwtPlugin(ctx.context);
|
|
3567
|
+
const jwtPluginOptions = jwtPlugin.options;
|
|
3568
|
+
let jwtPayload;
|
|
3473
3569
|
try {
|
|
3474
|
-
|
|
3570
|
+
jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
|
|
3475
3571
|
jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
|
|
3476
|
-
return (await jwtPlugin
|
|
3572
|
+
return (await jwtPlugin.endpoints.getJwks(ctx))?.response;
|
|
3477
3573
|
},
|
|
3478
3574
|
jwksCacheKey: jwtPlugin
|
|
3479
|
-
})), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL });
|
|
3480
|
-
const userInfoAudience = `${ctx.context.baseURL}/oauth2/userinfo`;
|
|
3481
|
-
if (!verified.payload.azp || !await isAudienceClaimAllowed(ctx, opts, verified.payload.aud, [userInfoAudience])) return null;
|
|
3575
|
+
})), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL })).payload;
|
|
3482
3576
|
} catch (error) {
|
|
3483
|
-
if (error instanceof
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
else if (error.name === "JWTInvalid") return null;
|
|
3490
|
-
throw error;
|
|
3491
|
-
}
|
|
3492
|
-
throw new Error(error);
|
|
3577
|
+
if (error instanceof errors.JWTExpired || error instanceof errors.JWTInvalid) return null;
|
|
3578
|
+
if (error instanceof TypeError || isInvalidJwtError(error)) throw new APIError$1("BAD_REQUEST", {
|
|
3579
|
+
error_description: "invalid JWT signature",
|
|
3580
|
+
error: "invalid_request"
|
|
3581
|
+
});
|
|
3582
|
+
throw error;
|
|
3493
3583
|
}
|
|
3584
|
+
const userInfoAudience = `${ctx.context.baseURL}/oauth2/userinfo`;
|
|
3585
|
+
if (!jwtPayload.azp || !await isAudienceClaimAllowed(ctx, opts, jwtPayload.aud, [userInfoAudience])) return null;
|
|
3494
3586
|
throw new APIError$1("BAD_REQUEST", {
|
|
3495
3587
|
error_description: "JWT access tokens are self-contained and cannot be revoked server-side",
|
|
3496
3588
|
error: "unsupported_token_type"
|
|
@@ -4443,6 +4535,10 @@ const oauthProvider = (options) => {
|
|
|
4443
4535
|
onRequest: handleIssuerMetadataRequest,
|
|
4444
4536
|
init: async (ctx) => {
|
|
4445
4537
|
if (ctx.options.secondaryStorage && ctx.options.session?.storeSessionInDatabase !== true) throw new BetterAuthError("OAuth Provider requires `session.storeSessionInDatabase: true` when using secondaryStorage");
|
|
4538
|
+
if (opts.allowClientKeyRotation) {
|
|
4539
|
+
const gap = ctx.adapter.id === "memory" ? "the memory adapter" : ctx.options.advanced?.database?.generateId === "serial" ? "`advanced.database.generateId: \"serial\"`" : void 0;
|
|
4540
|
+
if (gap) throw new BetterAuthError(`\`allowClientKeyRotation\` requires single-use client assertions, which ${gap} does not enforce`);
|
|
4541
|
+
}
|
|
4446
4542
|
await seedResourcesOnce(ctx, opts).catch((error) => logStartupSeedFailure(ctx, error));
|
|
4447
4543
|
logEnforcePerClientResourcesResolution(opts);
|
|
4448
4544
|
if (!opts.disableJwtPlugin) {
|
|
@@ -5257,6 +5353,7 @@ const oauthProvider = (options) => {
|
|
|
5257
5353
|
adminUpdateOAuthClient: adminUpdateOAuthClient(opts),
|
|
5258
5354
|
updateOAuthClient: updateOAuthClient(opts),
|
|
5259
5355
|
rotateClientSecret: rotateClientSecret(opts),
|
|
5356
|
+
rotateClientKeys: rotateClientKeys(opts),
|
|
5260
5357
|
deleteOAuthClient: deleteOAuthClient(opts),
|
|
5261
5358
|
getOAuthConsent: getOAuthConsent(opts),
|
|
5262
5359
|
getOAuthConsents: getOAuthConsents(opts),
|
|
@@ -5297,6 +5394,11 @@ const oauthProvider = (options) => {
|
|
|
5297
5394
|
window: opts.rateLimit?.register?.window ?? 60,
|
|
5298
5395
|
max: opts.rateLimit?.register?.max ?? 5
|
|
5299
5396
|
}] : [],
|
|
5397
|
+
...opts.rateLimit?.rotateClientKeys !== false ? [{
|
|
5398
|
+
pathMatcher: (path) => path === "/oauth2/client/rotate-keys",
|
|
5399
|
+
window: opts.rateLimit?.rotateClientKeys?.window ?? 60,
|
|
5400
|
+
max: opts.rateLimit?.rotateClientKeys?.max ?? 5
|
|
5401
|
+
}] : [],
|
|
5300
5402
|
...opts.rateLimit?.userinfo !== false ? [{
|
|
5301
5403
|
pathMatcher: (path) => path === "/oauth2/userinfo",
|
|
5302
5404
|
window: opts.rateLimit?.userinfo?.window ?? 60,
|
|
@@ -5606,7 +5708,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
5606
5708
|
if (promptSet?.has("select_account") && !opts.selectAccount?.page) return handleRedirect(ctx, getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
|
|
5607
5709
|
if (!(query.response_type === "code")) return handleRedirect(ctx, getErrorURL(ctx, "unsupported_response_type", "unsupported response type"));
|
|
5608
5710
|
const client = await getClient(ctx, opts, query.client_id);
|
|
5609
|
-
if (!client) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "
|
|
5711
|
+
if (!client) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "client not found"));
|
|
5610
5712
|
if (client.disabled) return handleRedirect(ctx, getErrorURL(ctx, "client_disabled", "client is disabled"));
|
|
5611
5713
|
if (!clientAllowsGrant(client, "authorization_code")) return handleRedirect(ctx, getErrorURL(ctx, "unauthorized_client", "client is not authorized to use the authorization_code grant"));
|
|
5612
5714
|
const registeredUris = client.redirectUris ?? [];
|
|
@@ -5712,7 +5814,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
5712
5814
|
}
|
|
5713
5815
|
}
|
|
5714
5816
|
if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
|
|
5715
|
-
const referenceId = await opts
|
|
5817
|
+
const referenceId = await getConsentReferenceId(opts, {
|
|
5716
5818
|
user: session.user,
|
|
5717
5819
|
session: session.session,
|
|
5718
5820
|
scopes: requestedScopes
|
|
@@ -5726,23 +5828,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
5726
5828
|
referenceId,
|
|
5727
5829
|
resource: requestedResources
|
|
5728
5830
|
});
|
|
5729
|
-
const consent = await ctx.
|
|
5730
|
-
model: "oauthConsent",
|
|
5731
|
-
where: [
|
|
5732
|
-
{
|
|
5733
|
-
field: "clientId",
|
|
5734
|
-
value: client.clientId
|
|
5735
|
-
},
|
|
5736
|
-
{
|
|
5737
|
-
field: "userId",
|
|
5738
|
-
value: session.user.id
|
|
5739
|
-
},
|
|
5740
|
-
...referenceId ? [{
|
|
5741
|
-
field: "referenceId",
|
|
5742
|
-
value: referenceId
|
|
5743
|
-
}] : []
|
|
5744
|
-
]
|
|
5745
|
-
});
|
|
5831
|
+
const consent = await findConsent(ctx, client.clientId, session.user.id, referenceId);
|
|
5746
5832
|
if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val)) || !requestedUserInfoClaims.every((claim) => (consent.requestedUserInfoClaims ?? []).includes(claim))) {
|
|
5747
5833
|
if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "consent_required", "End-User consent is required");
|
|
5748
5834
|
return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { o as OAuthClient } from "./oauth-DXiqbRzK.mjs";
|
|
2
|
+
import { JSONWebKeySet } from "jose";
|
|
3
|
+
|
|
4
|
+
//#region src/client-jwks.d.ts
|
|
5
|
+
/** A client's inline public JWK Set, as registration and key rotation accept it. */
|
|
6
|
+
type ClientJwks = NonNullable<OAuthClient["jwks"]>;
|
|
7
|
+
type PublicClientJwksValidationResult = {
|
|
8
|
+
valid: true;
|
|
9
|
+
jwks: JSONWebKeySet;
|
|
10
|
+
} | {
|
|
11
|
+
valid: false;
|
|
12
|
+
error: "jwks must be an RFC 7517 JWK Set object with a non-empty keys array" | "jwks must contain only public asymmetric keys" | "jwks keys must be supported public JWKs with required key parameters" | "jwks key alg must be supported for private_key_jwt and compatible with its key type and signing curve";
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Validates an OAuth client's public asymmetric JWK set.
|
|
16
|
+
*
|
|
17
|
+
* This boundary accepts only the RFC 7517 `{ keys: [...] }` representation.
|
|
18
|
+
* It performs no I/O and returns the validated set for downstream JOSE
|
|
19
|
+
* verification.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
declare function validatePublicClientJwks(input: unknown): PublicClientJwksValidationResult;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { PublicClientJwksValidationResult as n, validatePublicClientJwks as r, ClientJwks as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as ResourceServerMetadata } from "./oauth-
|
|
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
|
|
@@ -1084,14 +1084,21 @@ function getDpopProofJwt(ctx) {
|
|
|
1084
1084
|
return ctx.headers?.get("dpop") ?? void 0;
|
|
1085
1085
|
}
|
|
1086
1086
|
/**
|
|
1087
|
-
* The endpoint's public URL,
|
|
1088
|
-
*
|
|
1087
|
+
* The endpoint's public URL, which clients use as the DPoP `htu`: the origin
|
|
1088
|
+
* and base path of `baseURL` (not `request.url`, whose host proxies and dev
|
|
1089
|
+
* servers can rewrite) plus the request's own path below the base path, so
|
|
1090
|
+
* route params and a trailing slash match what the client called. `path` is
|
|
1091
|
+
* used when there is no HTTP request, such as an `auth.api` call.
|
|
1089
1092
|
*/
|
|
1090
1093
|
function getEndpointUrl(ctx, path) {
|
|
1091
1094
|
const { baseURL } = ctx.context;
|
|
1092
1095
|
let end = baseURL.length;
|
|
1093
1096
|
while (end > 0 && baseURL[end - 1] === "/") end--;
|
|
1094
|
-
|
|
1097
|
+
const base = baseURL.slice(0, end);
|
|
1098
|
+
if (!ctx.request || !base) return `${base}${path}`;
|
|
1099
|
+
const basePath = new URL(base).pathname.replace(/\/$/, "");
|
|
1100
|
+
const requestPath = new URL(ctx.request.url).pathname;
|
|
1101
|
+
return `${base}${requestPath.startsWith(`${basePath}/`) ? requestPath.slice(basePath.length) : path}`;
|
|
1095
1102
|
}
|
|
1096
1103
|
//#endregion
|
|
1097
1104
|
//#region src/oauthClient/client-credentials.ts
|
|
@@ -1971,6 +1978,10 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
1971
1978
|
error_description: "Either code_verifier or client_secret is required",
|
|
1972
1979
|
error: "invalid_request"
|
|
1973
1980
|
});
|
|
1981
|
+
/** Verify Client (RFC 6749 §5.2: an unknown or unauthenticated client is
|
|
1982
|
+
* `invalid_client`, so resolve it before the code lookup can report
|
|
1983
|
+
* `invalid_grant` and before a one-time code is consumed). */
|
|
1984
|
+
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, "authorization_code", authMethod);
|
|
1974
1985
|
/** Get and check Verification Value */
|
|
1975
1986
|
const { verificationValue, effectiveResources, authorizedResources, authorizationCodeId } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
|
|
1976
1987
|
const scopes = verificationValue.query.scope?.split(" ");
|
|
@@ -1978,8 +1989,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
1978
1989
|
error_description: "verification scope unset",
|
|
1979
1990
|
error: "invalid_scope"
|
|
1980
1991
|
});
|
|
1981
|
-
|
|
1982
|
-
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerified, "authorization_code", authMethod);
|
|
1992
|
+
validateClientScopes(client, scopes);
|
|
1983
1993
|
if (isPKCERequired(client, {
|
|
1984
1994
|
scopes: (verificationValue.query?.scope)?.split(" ") || [],
|
|
1985
1995
|
nonce: verificationValue.query?.nonce
|
|
@@ -1996,17 +2006,17 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
1996
2006
|
const pkceUsedInAuth = !!verificationValue.query?.code_challenge;
|
|
1997
2007
|
const pkceUsedInToken = !!code_verifier;
|
|
1998
2008
|
if (pkceUsedInAuth || pkceUsedInToken) {
|
|
1999
|
-
if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("
|
|
2009
|
+
if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("BAD_REQUEST", {
|
|
2000
2010
|
error_description: "code_verifier required because PKCE was used in authorization",
|
|
2001
2011
|
error: "invalid_request"
|
|
2002
2012
|
});
|
|
2003
|
-
if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("
|
|
2013
|
+
if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("BAD_REQUEST", {
|
|
2004
2014
|
error_description: "code_verifier provided but PKCE was not used in authorization",
|
|
2005
2015
|
error: "invalid_request"
|
|
2006
2016
|
});
|
|
2007
|
-
if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("
|
|
2017
|
+
if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("BAD_REQUEST", {
|
|
2008
2018
|
error_description: "code verification failed",
|
|
2009
|
-
error: "
|
|
2019
|
+
error: "invalid_grant"
|
|
2010
2020
|
});
|
|
2011
2021
|
}
|
|
2012
2022
|
/** Get user */
|
|
@@ -2109,6 +2119,10 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
2109
2119
|
error_description: "Missing a required refresh_token for refresh_token grant",
|
|
2110
2120
|
error: "invalid_request"
|
|
2111
2121
|
});
|
|
2122
|
+
/** Verify Client (RFC 6749 §5.2: an unknown or unauthenticated client is
|
|
2123
|
+
* `invalid_client`, so resolve it before the refresh token lookup can
|
|
2124
|
+
* report `invalid_grant`). */
|
|
2125
|
+
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, "refresh_token", authMethod);
|
|
2112
2126
|
const decodedRefresh = await decodeRefreshToken(opts, refresh_token);
|
|
2113
2127
|
const refreshToken = await ctx.context.adapter.findOne({
|
|
2114
2128
|
model: "oauthRefreshToken",
|
|
@@ -2142,7 +2156,7 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
2142
2156
|
error: "invalid_scope"
|
|
2143
2157
|
});
|
|
2144
2158
|
}
|
|
2145
|
-
|
|
2159
|
+
validateClientScopes(client, requestedScopes ?? scopes);
|
|
2146
2160
|
if (refreshToken.revoked) {
|
|
2147
2161
|
if (isWithinRefreshTokenReuseInterval(refreshToken)) {
|
|
2148
2162
|
const replay = await getRefreshTokenRotationReplay(ctx, refreshToken, await resolveRefreshTokenRotationReplayRequest(ctx, opts, {
|
|
@@ -2235,29 +2249,29 @@ async function isIntrospectionAuthorized(ctx, opts, params) {
|
|
|
2235
2249
|
* @returns RFC7662 introspection format
|
|
2236
2250
|
*/
|
|
2237
2251
|
async function validateJwtAccessToken(ctx, opts, token, clientId) {
|
|
2238
|
-
|
|
2239
|
-
|
|
2252
|
+
if (opts.disableJwtPlugin) throw new APIError$1("BAD_REQUEST", {
|
|
2253
|
+
error_description: "JWT access tokens are disabled",
|
|
2254
|
+
error: "invalid_request"
|
|
2255
|
+
});
|
|
2256
|
+
const jwtPlugin = getJwtPlugin(ctx.context);
|
|
2257
|
+
const jwtPluginOptions = jwtPlugin.options;
|
|
2240
2258
|
const userInfoAud = `${ctx.context.baseURL ?? ""}/oauth2/userinfo`;
|
|
2241
2259
|
const expectedIssuer = jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL;
|
|
2242
2260
|
let jwtPayload;
|
|
2243
2261
|
try {
|
|
2244
2262
|
jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
|
|
2245
2263
|
jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
|
|
2246
|
-
return (await jwtPlugin
|
|
2264
|
+
return (await jwtPlugin.endpoints.getJwks(ctx))?.response;
|
|
2247
2265
|
},
|
|
2248
2266
|
jwksCacheKey: jwtPlugin
|
|
2249
2267
|
})), { issuer: expectedIssuer })).payload;
|
|
2250
2268
|
} catch (error) {
|
|
2251
|
-
if (error instanceof
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
else if (error.name === "JWTInvalid") return { active: false };
|
|
2258
|
-
throw error;
|
|
2259
|
-
}
|
|
2260
|
-
throw new Error(error);
|
|
2269
|
+
if (error instanceof errors.JWTExpired || error instanceof errors.JWTInvalid) return { active: false };
|
|
2270
|
+
if (error instanceof TypeError || isInvalidJwtError(error)) throw new APIError$1("BAD_REQUEST", {
|
|
2271
|
+
error_description: "invalid JWT signature",
|
|
2272
|
+
error: "invalid_request"
|
|
2273
|
+
});
|
|
2274
|
+
throw error;
|
|
2261
2275
|
}
|
|
2262
2276
|
const rawAud = jwtPayload.aud;
|
|
2263
2277
|
if (!await isAudienceClaimAllowed(ctx, opts, rawAud, [userInfoAud])) return { active: false };
|
|
@@ -2548,4 +2562,4 @@ async function introspectEndpoint(ctx, opts) {
|
|
|
2548
2562
|
}
|
|
2549
2563
|
}
|
|
2550
2564
|
//#endregion
|
|
2551
|
-
export {
|
|
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.
|
|
@@ -1937,6 +1955,7 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
|
|
|
1937
1955
|
* introspect: { window: 60, max: 100 },
|
|
1938
1956
|
* revoke: { window: 60, max: 30 },
|
|
1939
1957
|
* register: { window: 60, max: 5 },
|
|
1958
|
+
* rotateClientKeys: { window: 60, max: 5 },
|
|
1940
1959
|
* userinfo: { window: 60, max: 60 },
|
|
1941
1960
|
* }
|
|
1942
1961
|
* ```
|
|
@@ -1982,6 +2001,14 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
|
|
|
1982
2001
|
window: number;
|
|
1983
2002
|
max: number;
|
|
1984
2003
|
} | false;
|
|
2004
|
+
/**
|
|
2005
|
+
* Rate limit for /oauth2/client/rotate-keys endpoint
|
|
2006
|
+
* @default { window: 60, max: 5 }
|
|
2007
|
+
*/
|
|
2008
|
+
rotateClientKeys?: {
|
|
2009
|
+
window: number;
|
|
2010
|
+
max: number;
|
|
2011
|
+
} | false;
|
|
1985
2012
|
/**
|
|
1986
2013
|
* Rate limit for /oauth2/userinfo endpoint
|
|
1987
2014
|
* @default { window: 60, max: 60 }
|
|
@@ -6,6 +6,7 @@ import { BetterAuthError } from "@better-auth/core/error";
|
|
|
6
6
|
import { CLIENT_ASSERTION_TYPE, decodeBasicCredentials } from "@better-auth/core/oauth2";
|
|
7
7
|
import { base64Url } from "@better-auth/utils/base64";
|
|
8
8
|
import { createHash } from "@better-auth/utils/hash";
|
|
9
|
+
import { errors } from "jose";
|
|
9
10
|
//#region src/extensions.ts
|
|
10
11
|
const DEFAULT_GRANT_TYPES = [
|
|
11
12
|
"authorization_code",
|
|
@@ -294,6 +295,21 @@ const getJwtPlugin = (ctx) => {
|
|
|
294
295
|
if (!plugin) throw new BetterAuthError("jwt_config");
|
|
295
296
|
return plugin;
|
|
296
297
|
};
|
|
298
|
+
const joseInfrastructureErrorCodes = new Set([
|
|
299
|
+
errors.JWKSTimeout.code,
|
|
300
|
+
errors.JWKSInvalid.code,
|
|
301
|
+
errors.JWKSMultipleMatchingKeys.code
|
|
302
|
+
]);
|
|
303
|
+
/**
|
|
304
|
+
* Whether a JWT verification error comes from the token itself (malformed,
|
|
305
|
+
* bad signature, unsupported alg, unknown kid, failed claim checks), as
|
|
306
|
+
* opposed to a key set failure that should still surface as a server error.
|
|
307
|
+
*
|
|
308
|
+
* @internal
|
|
309
|
+
*/
|
|
310
|
+
function isInvalidJwtError(error) {
|
|
311
|
+
return error instanceof errors.JOSEError && !joseInfrastructureErrorCodes.has(error.code);
|
|
312
|
+
}
|
|
297
313
|
/**
|
|
298
314
|
* Normalizes timestamp-like values returned by adapters.
|
|
299
315
|
*
|
|
@@ -371,6 +387,16 @@ async function verifyOAuthQueryParams(oauth_query, secret) {
|
|
|
371
387
|
return sigs.length === 1 && !!sig && constantTimeEqual(sig, verifySig) && /* @__PURE__ */ new Date(exp * 1e3) >= /* @__PURE__ */ new Date();
|
|
372
388
|
}
|
|
373
389
|
/**
|
|
390
|
+
* Rejects changes to a client listed in `cachedTrustedClients`. Cached trusted
|
|
391
|
+
* clients are immutable through the client management endpoints.
|
|
392
|
+
*/
|
|
393
|
+
function assertNotTrustedClient(opts, clientId) {
|
|
394
|
+
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("BAD_REQUEST", {
|
|
395
|
+
error: "invalid_client",
|
|
396
|
+
error_description: "trusted clients must be updated manually"
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
374
400
|
* Get a client by ID, checking trusted clients first, then database
|
|
375
401
|
*/
|
|
376
402
|
async function getClient(ctx, options, clientId) {
|
|
@@ -640,7 +666,7 @@ function throwInvalidClient(errorDescription, authentication) {
|
|
|
640
666
|
*/
|
|
641
667
|
async function validateClientCredentials(ctx, options, clientId, clientSecret, scopes, preVerified, grantType, authMethod) {
|
|
642
668
|
const client = await getClient(ctx, options, clientId);
|
|
643
|
-
if (!client) throwInvalidClient("
|
|
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",
|