@better-auth/oauth-provider 1.7.7 → 1.7.9
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-K1WHCNih.mjs → authorize-D8r6mvZx.mjs} +99 -59
- package/dist/client-resource.d.mts +1 -1
- 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 +3 -3
- package/dist/internal.d.mts +1 -1
- package/dist/internal.mjs +1 -1
- package/dist/{introspect-CNR06Oy3.mjs → introspect-7WCMPe5Z.mjs} +266 -266
- package/dist/{oauth-Dtt-caL2.d.mts → oauth-BWgg8X-A.d.mts} +3 -1
- package/dist/{oauth-C_7TvkRL.d.mts → oauth-DAMtT2vg.d.mts} +39 -1
- package/dist/{utils-D6Hp4Qd_.mjs → utils-DKU5wblX.mjs} +1 -1
- package/dist/{version-C2j2jEFL.mjs → version-DVUrz-nv.mjs} +1 -1
- package/package.json +8 -8
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
-
import { A as
|
|
3
|
-
import { B as validateOAuthProviderExtensions, C as storeToken, E as validateClientCredentials, I as getSupportedAuthMethods, L as getSupportedGrantTypes, O as verifyOAuthQueryParams, P as getClientDiscoveries, S as storeClientSecret, T as toResourceList, _ as removeMaxAgeFromQuery, a as getClient, b as resolveSubjectIdentifier, c as getStoredToken, d as mergeDiscoveryMetadata, g as parsePrompt, h as parseClientMetadata, i as extractClientCredentials, k as applyOAuthProviderMetadataExtensions, l as isPKCERequired, m as parseBearerToken, n as decryptStoredClientSecret, o as getJwtPlugin, r as destructureCredentials, t as clientAllowsGrant, u as isSessionFreshForSignedQuery, v as removePromptFromQuery, x as searchParamsToQuery, z as isExtensionTokenEndpointAuthMethod } from "./utils-
|
|
2
|
+
import { A as claimsRequestParameterSchema, C as clientJwksSchema, D as STANDARD_CLAIM_NAMES, E as STANDARD_CLAIMS, M as getRequestedUserInfoClaims, N as isValidOidcClaimsRequest, O as getSupportedClaims, S as authorizationQuerySchema, _ as logStartupSeedFailure, a as invalidateRefreshFamily, b as ResourceUriSchema, c as normalizeClientCredentialsScopes, d as assertIdentifierValid, f as extractRepeatedResourceFromForm, g as logEnforcePerClientResourcesResolution, h as isAudienceClaimAllowed, j as filterClaimsRequestUserInfoClaims, k as canSatisfyEssentialAcrRequest, l as validateClientCredentialsScopes, m as invalidateResourceCache, o as tokenEndpoint, p as getResource, r as decodeRefreshToken, s as userInfoEndpoint, t as introspectEndpoint, u as JWS_ALGORITHMS, v as resolveResourcePolicy, w as clientRegistrationRequestSchema, x as SafeUrlSchema, y as seedResourcesOnce } from "./introspect-7WCMPe5Z.mjs";
|
|
3
|
+
import { B as validateOAuthProviderExtensions, C as storeToken, E as validateClientCredentials, I as getSupportedAuthMethods, L as getSupportedGrantTypes, O as verifyOAuthQueryParams, P as getClientDiscoveries, S as storeClientSecret, T as toResourceList, _ as removeMaxAgeFromQuery, a as getClient, b as resolveSubjectIdentifier, c as getStoredToken, d as mergeDiscoveryMetadata, g as parsePrompt, h as parseClientMetadata, i as extractClientCredentials, k as applyOAuthProviderMetadataExtensions, l as isPKCERequired, m as parseBearerToken, n as decryptStoredClientSecret, o as getJwtPlugin, r as destructureCredentials, t as clientAllowsGrant, u as isSessionFreshForSignedQuery, v as removePromptFromQuery, x as searchParamsToQuery, z as isExtensionTokenEndpointAuthMethod } from "./utils-DKU5wblX.mjs";
|
|
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-DVUrz-nv.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";
|
|
@@ -16,12 +16,12 @@ import { BetterAuthError } from "@better-auth/core/error";
|
|
|
16
16
|
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
|
-
import { base64Url } from "@better-auth/utils/base64";
|
|
20
19
|
import { createHash } from "@better-auth/utils/hash";
|
|
21
20
|
import { getJwks, stripAccessTokenAuthorizationScheme } from "better-auth/oauth2";
|
|
22
21
|
import { compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify } from "jose";
|
|
22
|
+
import { isNativePrivateUseRedirectUri } from "@better-auth/core/utils/redirect-uri";
|
|
23
23
|
import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
|
|
24
|
-
import {
|
|
24
|
+
import { encodeDeterministicId } from "@better-auth/core/db/internal";
|
|
25
25
|
//#region src/consent.ts
|
|
26
26
|
async function consentEndpoint(ctx, opts, authorize) {
|
|
27
27
|
const oauthRequest = await oAuthState.get();
|
|
@@ -175,7 +175,10 @@ async function postLogin(ctx, authorize) {
|
|
|
175
175
|
ctx.headers?.set("accept", "application/json");
|
|
176
176
|
ctx.query = searchParamsToQuery(query);
|
|
177
177
|
const session = await getSessionFromCtx(ctx);
|
|
178
|
-
return await authorize(ctx, {
|
|
178
|
+
return await authorize(ctx, {
|
|
179
|
+
postLogin: state?.postLoginClearedForSession !== void 0 && state.postLoginClearedForSession === session?.session.id,
|
|
180
|
+
isPostLoginContinuation: true
|
|
181
|
+
});
|
|
179
182
|
}
|
|
180
183
|
//#endregion
|
|
181
184
|
//#region src/logout.ts
|
|
@@ -1172,7 +1175,7 @@ function stripReservedOAuthClientMetadataExtensions(metadata) {
|
|
|
1172
1175
|
* @throws APIError UNAUTHORIZED when there is no session or the hook denies the action.
|
|
1173
1176
|
* @throws APIError BAD_REQUEST when the request carries no headers.
|
|
1174
1177
|
*/
|
|
1175
|
-
async function assertClientPrivileges(ctx, session, opts, action) {
|
|
1178
|
+
async function assertClientPrivileges(ctx, session, opts, action, clientId) {
|
|
1176
1179
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1177
1180
|
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1178
1181
|
if (action === "configure-client-credentials-scopes" && !opts.clientPrivileges) throw new APIError("UNAUTHORIZED");
|
|
@@ -1180,7 +1183,36 @@ async function assertClientPrivileges(ctx, session, opts, action) {
|
|
|
1180
1183
|
headers: ctx.headers,
|
|
1181
1184
|
action,
|
|
1182
1185
|
session: session.session,
|
|
1183
|
-
user: session.user
|
|
1186
|
+
user: session.user,
|
|
1187
|
+
clientId
|
|
1188
|
+
})) throw new APIError("UNAUTHORIZED");
|
|
1189
|
+
}
|
|
1190
|
+
//#endregion
|
|
1191
|
+
//#region src/oauthResource/privileges.ts
|
|
1192
|
+
/**
|
|
1193
|
+
* Authorizes a resource action against the configured `resourcePrivileges`
|
|
1194
|
+
* hook. Gates every admin resource endpoint and the resource links selected
|
|
1195
|
+
* during managed client registration. Mirrors `assertClientPrivileges`:
|
|
1196
|
+
* a missing session → 401; a defined `resourcePrivileges` callback that
|
|
1197
|
+
* returns falsy → 401 with the original action context preserved.
|
|
1198
|
+
*
|
|
1199
|
+
* When `resourcePrivileges` is undefined, the gate degrades to "any
|
|
1200
|
+
* authenticated session can manage resources" — same forgiving default
|
|
1201
|
+
* as `clientPrivileges`. Operators who care about RBAC must define the
|
|
1202
|
+
* callback.
|
|
1203
|
+
*
|
|
1204
|
+
* @internal
|
|
1205
|
+
*/
|
|
1206
|
+
async function assertResourcePrivileges(ctx, session, opts, action, resourceId) {
|
|
1207
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1208
|
+
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1209
|
+
if (!opts.resourcePrivileges) return;
|
|
1210
|
+
if (!await opts.resourcePrivileges({
|
|
1211
|
+
headers: ctx.headers,
|
|
1212
|
+
action,
|
|
1213
|
+
session: session.session,
|
|
1214
|
+
user: session.user,
|
|
1215
|
+
resourceId
|
|
1184
1216
|
})) throw new APIError("UNAUTHORIZED");
|
|
1185
1217
|
}
|
|
1186
1218
|
//#endregion
|
|
@@ -1426,7 +1458,7 @@ async function consumeClientAssertion(ctx, opts, params) {
|
|
|
1426
1458
|
error: "invalid_client"
|
|
1427
1459
|
});
|
|
1428
1460
|
const jtiDigest = await createHash("SHA-256").digest(new TextEncoder().encode(`${namespace}:${payload.jti}`));
|
|
1429
|
-
const jtiId =
|
|
1461
|
+
const jtiId = encodeDeterministicId(new Uint8Array(jtiDigest).slice(0, 24), ctx.context.options);
|
|
1430
1462
|
try {
|
|
1431
1463
|
await ctx.context.adapter.create({
|
|
1432
1464
|
model: "oauthClientAssertion",
|
|
@@ -1637,14 +1669,31 @@ function applyOAuthClientRegistrationDefaults(client, defaultApplicationType = "
|
|
|
1637
1669
|
response_types: resolveRegistrationResponseTypes(client, grantTypes)
|
|
1638
1670
|
};
|
|
1639
1671
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
"
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1672
|
+
function isCustomSchemeWithHost(redirectUri) {
|
|
1673
|
+
const url = new URL(redirectUri);
|
|
1674
|
+
return url.protocol !== "http:" && url.protocol !== "https:" && url.host !== "";
|
|
1675
|
+
}
|
|
1676
|
+
function isNonHttpRedirectUri(redirectUri) {
|
|
1677
|
+
try {
|
|
1678
|
+
const protocol = new URL(redirectUri).protocol;
|
|
1679
|
+
return protocol !== "http:" && protocol !== "https:";
|
|
1680
|
+
} catch {
|
|
1681
|
+
return false;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Server-owned default for dynamic registrations that omit `application_type`.
|
|
1686
|
+
* `"infer"` classifies as native when any redirect URI uses a non-http(s)
|
|
1687
|
+
* scheme, because a web client can never register one. Every URI is then
|
|
1688
|
+
* validated against the native rules. A sent `application_type` is never
|
|
1689
|
+
* overridden because `applyOAuthClientRegistrationDefaults` only fills the
|
|
1690
|
+
* absent field.
|
|
1691
|
+
*/
|
|
1692
|
+
function resolveDynamicRegistrationDefaultApplicationType(opts, client) {
|
|
1693
|
+
const configured = opts.clientRegistrationDefaultApplicationType ?? "web";
|
|
1694
|
+
if (configured !== "infer") return configured;
|
|
1695
|
+
return (client.redirect_uris ?? []).some(isNonHttpRedirectUri) ? "native" : "web";
|
|
1696
|
+
}
|
|
1648
1697
|
function invalidRedirectUri(description) {
|
|
1649
1698
|
throw new APIError("BAD_REQUEST", {
|
|
1650
1699
|
error: "invalid_redirect_uri",
|
|
@@ -1661,13 +1710,15 @@ function getRawHttpHostname(redirectUri) {
|
|
|
1661
1710
|
}
|
|
1662
1711
|
return (hostAndPort.split(":")[0] ?? "").toLowerCase();
|
|
1663
1712
|
}
|
|
1664
|
-
|
|
1665
|
-
const
|
|
1666
|
-
const allowedResources = new Set([...defaultResources, ...opts.clientRegistrationAllowedResources ?? []]);
|
|
1713
|
+
function assertDynamicRegistrationResourcesAllowed(opts, requestedResources) {
|
|
1714
|
+
const allowedResources = new Set([...opts.clientRegistrationDefaultResources ?? [], ...opts.clientRegistrationAllowedResources ?? []]);
|
|
1667
1715
|
for (const identifier of requestedResources) if (!allowedResources.has(identifier)) throw new APIError("BAD_REQUEST", {
|
|
1668
1716
|
error: "invalid_target",
|
|
1669
1717
|
error_description: `requested resource ${identifier} is not allowed for client registration`
|
|
1670
1718
|
});
|
|
1719
|
+
}
|
|
1720
|
+
async function resolveClientRegistrationResources(ctx, opts, requestedResources) {
|
|
1721
|
+
const defaultResources = opts.clientRegistrationDefaultResources ?? [];
|
|
1671
1722
|
const resources = [...new Set([...defaultResources, ...requestedResources])];
|
|
1672
1723
|
for (const identifier of resources) {
|
|
1673
1724
|
const row = await getResource(ctx, opts, identifier);
|
|
@@ -1708,7 +1759,7 @@ function validateClientRedirectUri(redirectUri, applicationType) {
|
|
|
1708
1759
|
if (!isAllowedNativeHttpLoopback) invalidRedirectUri(`native clients may use http only on the exact loopback hosts localhost, 127.0.0.1, or [::1]: ${redirectUri}`);
|
|
1709
1760
|
return;
|
|
1710
1761
|
}
|
|
1711
|
-
if (
|
|
1762
|
+
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}`);
|
|
1712
1763
|
}
|
|
1713
1764
|
function assertValidRegistrationJwks(jwks) {
|
|
1714
1765
|
const result = validatePublicClientJwks(jwks);
|
|
@@ -1754,7 +1805,7 @@ async function registerEndpoint(ctx, opts) {
|
|
|
1754
1805
|
}
|
|
1755
1806
|
async function checkOAuthClient(client, opts, settings) {
|
|
1756
1807
|
const isClientMetadataDocument = settings?.registrationSource === "clientMetadataDocument";
|
|
1757
|
-
const clientWithDefaults = applyOAuthClientRegistrationDefaults(client, isClientMetadataDocument ? null : "web");
|
|
1808
|
+
const clientWithDefaults = applyOAuthClientRegistrationDefaults(client, isClientMetadataDocument ? null : settings?.registrationSource === "dynamic" ? resolveDynamicRegistrationDefaultApplicationType(opts, client) : "web");
|
|
1758
1809
|
const tokenEndpointAuthMethod = clientWithDefaults.token_endpoint_auth_method ?? "client_secret_basic";
|
|
1759
1810
|
if (!new Set(getSupportedAuthMethods(opts, { includeNone: true })).has(tokenEndpointAuthMethod)) throw new APIError("BAD_REQUEST", {
|
|
1760
1811
|
error: "invalid_client_metadata",
|
|
@@ -1803,6 +1854,10 @@ async function checkOAuthClient(client, opts, settings) {
|
|
|
1803
1854
|
error: "invalid_client_metadata",
|
|
1804
1855
|
error_description: "pairwise subject_type requires server pairwiseSecret configuration"
|
|
1805
1856
|
});
|
|
1857
|
+
if (clientWithDefaults.subject_type === "pairwise" && clientWithDefaults.redirect_uris?.some(isCustomSchemeWithHost)) throw new APIError("BAD_REQUEST", {
|
|
1858
|
+
error: "invalid_client_metadata",
|
|
1859
|
+
error_description: "pairwise clients cannot use custom-scheme redirect URIs that include a host"
|
|
1860
|
+
});
|
|
1806
1861
|
if (clientWithDefaults.subject_type === "pairwise" && clientWithDefaults.redirect_uris && clientWithDefaults.redirect_uris.length > 1) {
|
|
1807
1862
|
if (new Set(clientWithDefaults.redirect_uris.map((uri) => new URL(uri).host)).size > 1) throw new APIError("BAD_REQUEST", {
|
|
1808
1863
|
error: "invalid_client_metadata",
|
|
@@ -1914,10 +1969,11 @@ function isClientRegistrationCollision(error) {
|
|
|
1914
1969
|
* boundary.
|
|
1915
1970
|
*/
|
|
1916
1971
|
async function persistOAuthClientRegistration(ctx, opts, input) {
|
|
1917
|
-
const
|
|
1972
|
+
const registrationMetadata = input.registrationSource === "dynamic" && !input.metadata.scope ? {
|
|
1918
1973
|
...input.metadata,
|
|
1919
1974
|
scope: (opts.clientRegistrationDefaultScopes ?? opts.scopes)?.join(" ")
|
|
1920
|
-
} : input.metadata
|
|
1975
|
+
} : input.metadata;
|
|
1976
|
+
const body = applyOAuthClientRegistrationDefaults(registrationMetadata, input.registrationSource === "clientMetadataDocument" ? null : input.registrationSource === "dynamic" ? resolveDynamicRegistrationDefaultApplicationType(opts, registrationMetadata) : "web");
|
|
1921
1977
|
const isPublic = body.token_endpoint_auth_method === "none";
|
|
1922
1978
|
const isPrivateKeyJwt = body.token_endpoint_auth_method === "private_key_jwt";
|
|
1923
1979
|
const isExtensionAuthMethod = isExtensionTokenEndpointAuthMethod(opts, body.token_endpoint_auth_method);
|
|
@@ -1954,7 +2010,9 @@ async function persistOAuthClientRegistration(ctx, opts, input) {
|
|
|
1954
2010
|
schema.clientCredentialsScopes = input.registrationSource === "clientMetadataDocument" ? input.existingClient?.clientCredentialsScopes ?? [] : input.registrationSource === "managed" ? input.clientCredentialsScopes ?? [] : [];
|
|
1955
2011
|
schema.clientDiscoveryId = input.registrationSource === "clientMetadataDocument" ? input.clientDiscoveryId : null;
|
|
1956
2012
|
if (input.registrationSource === "clientMetadataDocument" && body.application_type === void 0) schema.applicationType = null;
|
|
1957
|
-
const
|
|
2013
|
+
const requestedResources = input.registrationSource === "clientMetadataDocument" ? [] : input.requestedResources ?? [];
|
|
2014
|
+
if (input.registrationSource === "dynamic") assertDynamicRegistrationResourcesAllowed(opts, requestedResources);
|
|
2015
|
+
const resources = await resolveClientRegistrationResources(ctx, opts, requestedResources);
|
|
1958
2016
|
const clientModel = opts.schema?.oauthClient?.modelName ?? "oauthClient";
|
|
1959
2017
|
const clientResourceModel = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
|
|
1960
2018
|
const existingClient = input.registrationSource === "clientMetadataDocument" ? input.existingClient : void 0;
|
|
@@ -2175,12 +2233,15 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
|
2175
2233
|
const clientCredentialsScopes = settings?.admin ? normalizeClientCredentialsScopes(rawClientCredentialsScopes ?? []) : [];
|
|
2176
2234
|
validateClientCredentialsScopes(clientCredentialsScopes, resolveRegistrationGrantTypes(metadata), metadata.token_endpoint_auth_method, opts);
|
|
2177
2235
|
if (clientCredentialsScopes.length > 0) await assertClientPrivileges(ctx, session, opts, "configure-client-credentials-scopes");
|
|
2236
|
+
const requestedResources = settings?.admin ? metadata.resources : void 0;
|
|
2237
|
+
for (const identifier of new Set(requestedResources ?? [])) await assertResourcePrivileges(ctx, session, opts, "link", identifier);
|
|
2178
2238
|
const registrationInput = {
|
|
2179
2239
|
metadata,
|
|
2180
2240
|
registrationSource: "managed",
|
|
2181
2241
|
userId: referenceId ? void 0 : session.session.userId,
|
|
2182
2242
|
referenceId,
|
|
2183
|
-
clientCredentialsScopes
|
|
2243
|
+
clientCredentialsScopes,
|
|
2244
|
+
...requestedResources ? { requestedResources } : {}
|
|
2184
2245
|
};
|
|
2185
2246
|
const responseBody = settings?.admin ? await createOAuthClientAdministrativeRegistration(ctx, opts, registrationInput) : await createOAuthClientRegistration(ctx, opts, registrationInput);
|
|
2186
2247
|
ctx.setStatus(201);
|
|
@@ -2280,7 +2341,7 @@ function schemaToOAuth(input) {
|
|
|
2280
2341
|
//#region src/oauthClient/endpoints.ts
|
|
2281
2342
|
async function getClientEndpoint(ctx, opts) {
|
|
2282
2343
|
const session = await getSessionFromCtx(ctx);
|
|
2283
|
-
await assertClientPrivileges(ctx, session, opts, "read");
|
|
2344
|
+
await assertClientPrivileges(ctx, session, opts, "read", ctx.query.client_id);
|
|
2284
2345
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
2285
2346
|
const client = await getClient(ctx, opts, ctx.query.client_id);
|
|
2286
2347
|
if (!client) throw new APIError("NOT_FOUND", {
|
|
@@ -2357,7 +2418,7 @@ async function getClientsEndpoint(ctx, opts) {
|
|
|
2357
2418
|
}
|
|
2358
2419
|
async function deleteClientEndpoint(ctx, opts) {
|
|
2359
2420
|
const session = await getSessionFromCtx(ctx);
|
|
2360
|
-
await assertClientPrivileges(ctx, session, opts, "delete");
|
|
2421
|
+
await assertClientPrivileges(ctx, session, opts, "delete", ctx.body.client_id);
|
|
2361
2422
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
2362
2423
|
const clientId = ctx.body.client_id;
|
|
2363
2424
|
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
@@ -2384,7 +2445,7 @@ async function deleteClientEndpoint(ctx, opts) {
|
|
|
2384
2445
|
}
|
|
2385
2446
|
async function updateClientEndpoint(ctx, opts, settings) {
|
|
2386
2447
|
const session = await getSessionFromCtx(ctx);
|
|
2387
|
-
await assertClientPrivileges(ctx, session, opts, "update");
|
|
2448
|
+
await assertClientPrivileges(ctx, session, opts, "update", ctx.body.client_id);
|
|
2388
2449
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
2389
2450
|
const clientId = ctx.body.client_id;
|
|
2390
2451
|
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
@@ -2400,7 +2461,7 @@ async function updateClientEndpoint(ctx, opts, settings) {
|
|
|
2400
2461
|
const ownsClient = client.userId ? client.userId === session.user.id : client.referenceId && opts.clientReference ? client.referenceId === await opts.clientReference(session) : false;
|
|
2401
2462
|
const isCrossOwnerScopeConfiguration = !ownsClient && settings?.admin && rawClientCredentialsScopes !== void 0 && Object.keys(updates).length === 0;
|
|
2402
2463
|
if (!ownsClient && !isCrossOwnerScopeConfiguration) throw new APIError("UNAUTHORIZED");
|
|
2403
|
-
if (isCrossOwnerScopeConfiguration) await assertClientPrivileges(ctx, session, opts, "configure-client-credentials-scopes");
|
|
2464
|
+
if (isCrossOwnerScopeConfiguration) await assertClientPrivileges(ctx, session, opts, "configure-client-credentials-scopes", clientId);
|
|
2404
2465
|
if (Object.keys(updates).length === 0 && rawClientCredentialsScopes === void 0) {
|
|
2405
2466
|
const res = schemaToOAuth(client);
|
|
2406
2467
|
res.client_secret = void 0;
|
|
@@ -2415,7 +2476,7 @@ async function updateClientEndpoint(ctx, opts, settings) {
|
|
|
2415
2476
|
const clientCredentialsScopes = rawClientCredentialsScopes === void 0 ? void 0 : normalizeClientCredentialsScopes(rawClientCredentialsScopes);
|
|
2416
2477
|
if (clientCredentialsScopes !== void 0) {
|
|
2417
2478
|
validateClientCredentialsScopes(clientCredentialsScopes, finalGrantTypes, finalTokenEndpointAuthMethod, opts);
|
|
2418
|
-
if (clientCredentialsScopes.length > 0 && !isCrossOwnerScopeConfiguration) await assertClientPrivileges(ctx, session, opts, "configure-client-credentials-scopes");
|
|
2479
|
+
if (clientCredentialsScopes.length > 0 && !isCrossOwnerScopeConfiguration) await assertClientPrivileges(ctx, session, opts, "configure-client-credentials-scopes", clientId);
|
|
2419
2480
|
}
|
|
2420
2481
|
await checkOAuthClient({
|
|
2421
2482
|
...schemaToOAuth(client),
|
|
@@ -2455,7 +2516,7 @@ async function updateClientEndpoint(ctx, opts, settings) {
|
|
|
2455
2516
|
}
|
|
2456
2517
|
async function rotateClientSecretEndpoint(ctx, opts) {
|
|
2457
2518
|
const session = await getSessionFromCtx(ctx);
|
|
2458
|
-
await assertClientPrivileges(ctx, session, opts, "rotate");
|
|
2519
|
+
await assertClientPrivileges(ctx, session, opts, "rotate", ctx.body.client_id);
|
|
2459
2520
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
2460
2521
|
const clientId = ctx.body.client_id;
|
|
2461
2522
|
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
@@ -2525,6 +2586,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
2525
2586
|
jwks_uri: z.string().optional(),
|
|
2526
2587
|
grant_types: grantTypesSchema.optional(),
|
|
2527
2588
|
client_credentials_scopes: z.array(z.string().trim().min(1)).optional(),
|
|
2589
|
+
resources: z.array(ResourceUriSchema).optional(),
|
|
2528
2590
|
response_types: z.array(z.enum(["code"])).optional(),
|
|
2529
2591
|
client_secret_expires_at: z.union([z.string(), z.number()]).optional().default(0),
|
|
2530
2592
|
skip_consent: z.boolean().optional(),
|
|
@@ -2889,6 +2951,7 @@ const adminUpdateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/updat
|
|
|
2889
2951
|
client_secret_expires_at: z.union([z.string(), z.number()]).optional(),
|
|
2890
2952
|
skip_consent: z.boolean().optional(),
|
|
2891
2953
|
enable_end_session: z.boolean().optional(),
|
|
2954
|
+
disabled: z.boolean().optional(),
|
|
2892
2955
|
dpop_bound_access_tokens: z.boolean().optional(),
|
|
2893
2956
|
metadata: z.record(z.string(), z.unknown()).optional()
|
|
2894
2957
|
})
|
|
@@ -3085,30 +3148,6 @@ const deleteOAuthConsent = (opts) => createAuthEndpoint("/oauth2/delete-consent"
|
|
|
3085
3148
|
});
|
|
3086
3149
|
//#endregion
|
|
3087
3150
|
//#region src/oauthResource/endpoints.ts
|
|
3088
|
-
/**
|
|
3089
|
-
* Gate every admin resource endpoint. Mirrors `assertClientPrivileges`:
|
|
3090
|
-
* a missing session → 401; a defined `resourcePrivileges` callback that
|
|
3091
|
-
* returns falsy → 401 with the original action context preserved.
|
|
3092
|
-
*
|
|
3093
|
-
* When `resourcePrivileges` is undefined, the gate degrades to "any
|
|
3094
|
-
* authenticated session can manage resources" — same forgiving default
|
|
3095
|
-
* as `clientPrivileges`. Operators who care about RBAC must define the
|
|
3096
|
-
* callback.
|
|
3097
|
-
*
|
|
3098
|
-
* @internal
|
|
3099
|
-
*/
|
|
3100
|
-
async function assertResourcePrivileges(ctx, session, opts, action, resourceId) {
|
|
3101
|
-
if (!session) throw new APIError("UNAUTHORIZED");
|
|
3102
|
-
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
3103
|
-
if (!opts.resourcePrivileges) return;
|
|
3104
|
-
if (!await opts.resourcePrivileges({
|
|
3105
|
-
headers: ctx.headers,
|
|
3106
|
-
action,
|
|
3107
|
-
session: session.session,
|
|
3108
|
-
user: session.user,
|
|
3109
|
-
resourceId
|
|
3110
|
-
})) throw new APIError("UNAUTHORIZED");
|
|
3111
|
-
}
|
|
3112
3151
|
const resourceModel = (opts) => opts.schema?.oauthResource?.modelName ?? "oauthResource";
|
|
3113
3152
|
const linkModel = (opts) => opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
|
|
3114
3153
|
const clientModel = (opts) => opts.schema?.oauthClient?.modelName ?? "oauthClient";
|
|
@@ -4404,7 +4443,7 @@ const oauthProvider = (options) => {
|
|
|
4404
4443
|
onRequest: handleIssuerMetadataRequest,
|
|
4405
4444
|
init: async (ctx) => {
|
|
4406
4445
|
if (ctx.options.secondaryStorage && ctx.options.session?.storeSessionInDatabase !== true) throw new BetterAuthError("OAuth Provider requires `session.storeSessionInDatabase: true` when using secondaryStorage");
|
|
4407
|
-
await
|
|
4446
|
+
await seedResourcesOnce(ctx, opts).catch((error) => logStartupSeedFailure(ctx, error));
|
|
4408
4447
|
logEnforcePerClientResourcesResolution(opts);
|
|
4409
4448
|
if (!opts.disableJwtPlugin) {
|
|
4410
4449
|
const jwtPluginOptions = getJwtPlugin(ctx)?.options;
|
|
@@ -4557,7 +4596,7 @@ const oauthProvider = (options) => {
|
|
|
4557
4596
|
body: z.object({
|
|
4558
4597
|
selected: z.boolean().optional().meta({ description: "Confirms an account has been selected and authorization can proceed." }),
|
|
4559
4598
|
created: z.boolean().optional().meta({ description: "Confirms an account was registered" }),
|
|
4560
|
-
postLogin: z.boolean().optional().meta({ description: "
|
|
4599
|
+
postLogin: z.boolean().optional().meta({ description: "Resumes after the post login page. postLogin.shouldRedirect runs again with isContinuation: true." }),
|
|
4561
4600
|
oauth_query: z.string().optional().meta({ description: "The redirected page's query parameters" })
|
|
4562
4601
|
}),
|
|
4563
4602
|
use: [sessionMiddleware],
|
|
@@ -5665,7 +5704,8 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
5665
5704
|
headers: request.headers,
|
|
5666
5705
|
user: session.user,
|
|
5667
5706
|
session: session.session,
|
|
5668
|
-
scopes: requestedScopes
|
|
5707
|
+
scopes: requestedScopes,
|
|
5708
|
+
isContinuation: settings?.isPostLoginContinuation ?? false
|
|
5669
5709
|
})) {
|
|
5670
5710
|
if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "interaction_required", "End-User interaction is required");
|
|
5671
5711
|
return redirectWithPromptCode(ctx, opts, "post_login");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as ResourceServerMetadata } from "./oauth-
|
|
1
|
+
import { c as ResourceServerMetadata } from "./oauth-DAMtT2vg.mjs";
|
|
2
2
|
import { ResourceRequestInput, VerifyAccessTokenRequestOptions } from "better-auth/oauth2";
|
|
3
3
|
import { JWTPayload, JWTVerifyOptions } from "jose";
|
|
4
4
|
import { BetterAuthOptions } from "better-auth/types";
|
package/dist/client-resource.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { o as getJwtPlugin, s as getOAuthProviderPlugin } from "./utils-
|
|
2
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
1
|
+
import { o as getJwtPlugin, s as getOAuthProviderPlugin } from "./utils-DKU5wblX.mjs";
|
|
2
|
+
import { t as PACKAGE_VERSION } from "./version-DVUrz-nv.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-BWgg8X-A.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-DVUrz-nv.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-DAMtT2vg.mjs";
|
|
2
|
+
import { a as OAuthEndpointErrorResult, c as OAuthFieldErrorCode, d as DEVICE_CODE_GRANT_TYPE, f as OAuthDeviceAuthorizationOptions, i as getIssuer, l as OAuthFieldErrorCodeMap, n as getOAuthProviderState, o as OAuthEndpointRedirectContext, p as oauthDeviceAuthorization, r as oauthProvider, s as OAuthErrorCode, t as DEFAULT_OAUTH_SCOPES, u as OAuthRedirectOnError } from "./oauth-BWgg8X-A.mjs";
|
|
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 { D as validateClientScopes, N as extendOAuthProvider, O as verifyOAuthQueryParams, T as toResourceList, a as getClient, f as normalizeClientAuthenticationParameters, s as getOAuthProviderPlugin, w as toAudienceClaim } from "./utils-
|
|
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 { T as oauthClientMetadataSchema, b as ResourceUriSchema, f as extractRepeatedResourceFromForm, i as getOAuthProviderApi, v as resolveResourcePolicy } from "./introspect-7WCMPe5Z.mjs";
|
|
2
|
+
import { D as validateClientScopes, N as extendOAuthProvider, O as verifyOAuthQueryParams, T as toResourceList, a as getClient, f as normalizeClientAuthenticationParameters, s as getOAuthProviderPlugin, w as toAudienceClaim } from "./utils-DKU5wblX.mjs";
|
|
3
|
+
import { d as metadataResponse, f as oauthAuthorizationServerMetadata, h as oidcServerMetadata, i as oauthProvider, m as oauthProviderOpenIdConfigMetadata, n as DEFAULT_OAUTH_SCOPES, p as oauthProviderAuthServerMetadata, r as getOAuthProviderState, s as consumeClientAssertion, t as getIssuer, u as authServerMetadata } from "./authorize-D8r6mvZx.mjs";
|
|
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";
|
package/dist/internal.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { H as Scope, V as SchemaClient, j as OAuthOptions, o as OAuthClient } from "./oauth-
|
|
1
|
+
import { H as Scope, V as SchemaClient, j as OAuthOptions, o as OAuthClient } from "./oauth-DAMtT2vg.mjs";
|
|
2
2
|
import { JSONWebKeySet } from "jose";
|
|
3
3
|
import { GenericEndpointContext } from "@better-auth/core";
|
|
4
4
|
|
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-D8r6mvZx.mjs";
|
|
2
2
|
export { isForbiddenCimdClientMetadataField, registerClientMetadataDocument, validatePublicClientJwks };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
-
import { A as collectExtensionAccessTokenClaims, C as storeToken, E as validateClientCredentials, F as getExtensionGrantHandler, L as getSupportedGrantTypes, M as collectExtensionUserInfoClaims, R as hasUserInfoClaimExtension, T as toResourceList, a as getClient, b as resolveSubjectIdentifier, c as getStoredToken, h as parseClientMetadata, i as extractClientCredentials, j as collectExtensionIdTokenClaims, l as isPKCERequired, n as decryptStoredClientSecret, o as getJwtPlugin, p as normalizeTimestampValue, r as destructureCredentials, t as clientAllowsGrant, w as toAudienceClaim, y as resolveSessionAuthTime } from "./utils-
|
|
2
|
+
import { A as collectExtensionAccessTokenClaims, C as storeToken, E as validateClientCredentials, F as getExtensionGrantHandler, L as getSupportedGrantTypes, M as collectExtensionUserInfoClaims, R as hasUserInfoClaimExtension, T as toResourceList, a as getClient, b as resolveSubjectIdentifier, c as getStoredToken, h as parseClientMetadata, i as extractClientCredentials, j as collectExtensionIdTokenClaims, l as isPKCERequired, n as decryptStoredClientSecret, o as getJwtPlugin, p as normalizeTimestampValue, r as destructureCredentials, t as clientAllowsGrant, w as toAudienceClaim, y as resolveSessionAuthTime } from "./utils-DKU5wblX.mjs";
|
|
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";
|
|
@@ -7,8 +7,8 @@ import { logger } from "@better-auth/core/env";
|
|
|
7
7
|
import * as z from "zod";
|
|
8
8
|
import { createDpopReplayStore, enforceDpopBinding, generateCodeChallenge, getConfirmationJkt, getDpopJktFromPayload, getJwks, isDpopBindingError, isDpopProofError, parseAccessTokenAuthorization, stripAccessTokenAuthorizationScheme, verifyDpopProof } from "better-auth/oauth2";
|
|
9
9
|
import { SignJWT, base64url, createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
|
|
10
|
-
import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
|
|
11
10
|
import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
|
|
11
|
+
import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
|
|
12
12
|
//#region src/authentication-context.ts
|
|
13
13
|
const RESERVED_ID_TOKEN_CLAIMS = new Set([
|
|
14
14
|
"iss",
|
|
@@ -265,6 +265,187 @@ async function resolveAccessTokenClaims(input) {
|
|
|
265
265
|
});
|
|
266
266
|
}
|
|
267
267
|
//#endregion
|
|
268
|
+
//#region src/types/zod.ts
|
|
269
|
+
const DANGEROUS_SCHEMES = [
|
|
270
|
+
"javascript:",
|
|
271
|
+
"data:",
|
|
272
|
+
"vbscript:"
|
|
273
|
+
];
|
|
274
|
+
/**
|
|
275
|
+
* Validates an RFC 8707 resource indicator. The value must be an absolute URI
|
|
276
|
+
* with no fragment (RFC 8707 §2). Unlike a redirect URI it is not restricted to
|
|
277
|
+
* HTTPS, because a resource server identifier may use any absolute URI scheme;
|
|
278
|
+
* configured OAuth resources are the authoritative control over
|
|
279
|
+
* which resources a token may target.
|
|
280
|
+
*/
|
|
281
|
+
const ResourceUriSchema = z.string().superRefine((val, ctx) => {
|
|
282
|
+
if (!URL.canParse(val)) {
|
|
283
|
+
ctx.addIssue({
|
|
284
|
+
code: "custom",
|
|
285
|
+
message: "resource must be an absolute URI",
|
|
286
|
+
fatal: true
|
|
287
|
+
});
|
|
288
|
+
return z.NEVER;
|
|
289
|
+
}
|
|
290
|
+
if (val.includes("#")) {
|
|
291
|
+
ctx.addIssue({
|
|
292
|
+
code: "custom",
|
|
293
|
+
message: "resource must not contain a fragment"
|
|
294
|
+
});
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (DANGEROUS_SCHEMES.includes(new URL(val).protocol)) ctx.addIssue({
|
|
298
|
+
code: "custom",
|
|
299
|
+
message: "resource cannot use javascript:, data:, or vbscript: scheme"
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
const authorizationPromptTokenSchema = z.enum([
|
|
303
|
+
"none",
|
|
304
|
+
"consent",
|
|
305
|
+
"login",
|
|
306
|
+
"create",
|
|
307
|
+
"select_account"
|
|
308
|
+
]);
|
|
309
|
+
const authorizationPromptSchema = z.string().superRefine((value, ctx) => {
|
|
310
|
+
const promptTokens = value.split(" ").map((token) => token.trim()).filter(Boolean);
|
|
311
|
+
const promptSet = /* @__PURE__ */ new Set();
|
|
312
|
+
if (!promptTokens.length) {
|
|
313
|
+
ctx.addIssue({
|
|
314
|
+
code: "custom",
|
|
315
|
+
message: "prompt must include at least one value"
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
for (const token of promptTokens) {
|
|
320
|
+
const result = authorizationPromptTokenSchema.safeParse(token);
|
|
321
|
+
if (!result.success) {
|
|
322
|
+
ctx.addIssue({
|
|
323
|
+
code: "custom",
|
|
324
|
+
message: `unsupported prompt value: ${token}`
|
|
325
|
+
});
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
promptSet.add(result.data);
|
|
329
|
+
}
|
|
330
|
+
if (promptSet.has("none") && promptSet.size > 1) ctx.addIssue({
|
|
331
|
+
code: "custom",
|
|
332
|
+
message: "prompt=none cannot be combined with other prompt values"
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
/** Canonical RFC 7517 JWK Set metadata for an OAuth client. */
|
|
336
|
+
const clientJwksSchema = z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) });
|
|
337
|
+
const maxAgeSchema = z.union([z.number(), z.string().trim().min(1)]).transform((value, ctx) => {
|
|
338
|
+
const maxAge = typeof value === "number" ? value : Number(value);
|
|
339
|
+
if (!Number.isInteger(maxAge) || maxAge < 0) {
|
|
340
|
+
ctx.addIssue({
|
|
341
|
+
code: "custom",
|
|
342
|
+
message: "max_age must be a non-negative integer"
|
|
343
|
+
});
|
|
344
|
+
return z.NEVER;
|
|
345
|
+
}
|
|
346
|
+
return maxAge;
|
|
347
|
+
});
|
|
348
|
+
const dpopJktSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/, "dpop_jkt must be a base64url-encoded SHA-256 JWK thumbprint");
|
|
349
|
+
/**
|
|
350
|
+
* Runtime schema for OAuthAuthorizationQuery.
|
|
351
|
+
* Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
|
|
352
|
+
*/
|
|
353
|
+
const authorizationQuerySchema = z.object({
|
|
354
|
+
response_type: z.string().pipe(z.enum(["code"])).optional(),
|
|
355
|
+
request: z.string().optional(),
|
|
356
|
+
request_uri: z.string().optional(),
|
|
357
|
+
redirect_uri: SafeUrlSchema.optional(),
|
|
358
|
+
scope: z.string().optional(),
|
|
359
|
+
state: z.string().optional(),
|
|
360
|
+
client_id: z.string().min(1, "client_id is required"),
|
|
361
|
+
prompt: authorizationPromptSchema.optional(),
|
|
362
|
+
display: z.string().optional(),
|
|
363
|
+
ui_locales: z.string().optional(),
|
|
364
|
+
max_age: maxAgeSchema.optional(),
|
|
365
|
+
acr_values: z.string().optional(),
|
|
366
|
+
login_hint: z.string().optional(),
|
|
367
|
+
id_token_hint: z.string().optional(),
|
|
368
|
+
code_challenge: z.string().optional(),
|
|
369
|
+
code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
|
|
370
|
+
nonce: z.string().optional(),
|
|
371
|
+
claims: claimsRequestInputSchema.optional(),
|
|
372
|
+
dpop_jkt: dpopJktSchema.optional(),
|
|
373
|
+
resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional()
|
|
374
|
+
}).passthrough();
|
|
375
|
+
const storedAuthorizationQuerySchema = authorizationQuerySchema.extend({
|
|
376
|
+
redirect_uri: SafeUrlSchema.optional(),
|
|
377
|
+
claims: claimsRequestParameterSchema.optional()
|
|
378
|
+
});
|
|
379
|
+
/**
|
|
380
|
+
* Runtime schema for the authorization code verification value.
|
|
381
|
+
* Validates structure on deserialization from the JSON blob stored in the DB.
|
|
382
|
+
* Uses passthrough so future fields (e.g. from authorization challenge) don't break parsing.
|
|
383
|
+
*/
|
|
384
|
+
const verificationValueSchema = z.object({
|
|
385
|
+
type: z.literal("authorization_code"),
|
|
386
|
+
query: storedAuthorizationQuerySchema,
|
|
387
|
+
sessionId: z.string(),
|
|
388
|
+
userId: z.string(),
|
|
389
|
+
referenceId: z.string().optional(),
|
|
390
|
+
authTime: z.number().optional(),
|
|
391
|
+
resource: z.array(z.string()).optional()
|
|
392
|
+
}).passthrough();
|
|
393
|
+
/**
|
|
394
|
+
* Request body accepted at `POST /oauth2/register` (RFC 7591 §2 client
|
|
395
|
+
* metadata). This is the single source of truth for the registration contract:
|
|
396
|
+
* the endpoint validates against it and {@link ClientRegistrationRequest} is
|
|
397
|
+
* inferred from it, so the type a `validateInitialAccessToken` callback receives
|
|
398
|
+
* always matches what is actually validated. `grant_types` and
|
|
399
|
+
* `token_endpoint_auth_method` are open strings because extensions can register
|
|
400
|
+
* custom values. Server-assigned fields (`client_id`, `client_secret`, the
|
|
401
|
+
* issued/expiry timestamps) and internal state (`disabled`, `reference_id`) are
|
|
402
|
+
* never part of a registration request.
|
|
403
|
+
*
|
|
404
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7591#section-2
|
|
405
|
+
*/
|
|
406
|
+
const clientRegistrationRequestSchema = z.object({
|
|
407
|
+
redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
408
|
+
scope: z.string().optional(),
|
|
409
|
+
client_name: z.string().refine((value) => value.trim().length > 0, "client_name cannot be empty").optional(),
|
|
410
|
+
client_uri: z.string().optional(),
|
|
411
|
+
logo_uri: z.string().optional(),
|
|
412
|
+
contacts: z.array(z.string().min(1)).min(1).optional(),
|
|
413
|
+
tos_uri: z.string().optional(),
|
|
414
|
+
policy_uri: z.string().optional(),
|
|
415
|
+
software_id: z.string().optional(),
|
|
416
|
+
software_version: z.string().optional(),
|
|
417
|
+
software_statement: z.string().optional(),
|
|
418
|
+
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
419
|
+
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
420
|
+
backchannel_logout_session_required: z.boolean().optional(),
|
|
421
|
+
token_endpoint_auth_method: z.string().trim().min(1).optional(),
|
|
422
|
+
jwks: clientJwksSchema.optional(),
|
|
423
|
+
jwks_uri: z.string().optional(),
|
|
424
|
+
grant_types: z.array(z.string().trim().min(1)).min(1).optional(),
|
|
425
|
+
response_types: z.array(z.enum(["code"])).optional(),
|
|
426
|
+
application_type: z.enum(["web", "native"]).optional(),
|
|
427
|
+
subject_type: z.enum(["public", "pairwise"]).optional(),
|
|
428
|
+
dpop_bound_access_tokens: z.boolean().optional(),
|
|
429
|
+
resources: z.array(ResourceUriSchema).optional(),
|
|
430
|
+
skip_consent: z.never({ error: "skip_consent cannot be set during dynamic client registration" }).optional()
|
|
431
|
+
});
|
|
432
|
+
/**
|
|
433
|
+
* Complete OAuth client metadata document schema shared by registration and
|
|
434
|
+
* verified client-discovery integrations.
|
|
435
|
+
*
|
|
436
|
+
* Unlike {@link clientRegistrationRequestSchema}, a metadata document carries
|
|
437
|
+
* its own identifier and may contain the server-assigned secret fields that a
|
|
438
|
+
* discovery profile must explicitly prohibit. Keeping those fields in the
|
|
439
|
+
* parsed shape lets profile validation distinguish a forbidden value from an
|
|
440
|
+
* unknown extension without unsafe casts or ad-hoc allowlists.
|
|
441
|
+
*/
|
|
442
|
+
const oauthClientMetadataSchema = clientRegistrationRequestSchema.omit({ skip_consent: true }).extend({
|
|
443
|
+
client_id: z.string().refine((value) => value.trim().length > 0, "client_id cannot be empty"),
|
|
444
|
+
client_secret: z.string().optional(),
|
|
445
|
+
client_secret_expires_at: z.number().int().nonnegative().optional(),
|
|
446
|
+
client_id_issued_at: z.number().int().nonnegative().optional()
|
|
447
|
+
}).loose();
|
|
448
|
+
//#endregion
|
|
268
449
|
//#region src/resources.ts
|
|
269
450
|
/**
|
|
270
451
|
* Source-of-truth list of asymmetric JWS algorithms supported by the JWT
|
|
@@ -317,18 +498,10 @@ async function checkIdentifier(opts, identifier) {
|
|
|
317
498
|
};
|
|
318
499
|
return { ok: true };
|
|
319
500
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
url = new URL(identifier);
|
|
323
|
-
} catch {
|
|
324
|
-
return {
|
|
325
|
-
ok: false,
|
|
326
|
-
reason: `resource identifier ${identifier} must be an absolute URI (RFC 8707 §2)`
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
if (url.hash) return {
|
|
501
|
+
const parsed = ResourceUriSchema.safeParse(identifier);
|
|
502
|
+
if (!parsed.success) return {
|
|
330
503
|
ok: false,
|
|
331
|
-
reason: `resource identifier ${identifier}
|
|
504
|
+
reason: `resource identifier ${identifier} is invalid: ${parsed.error.issues[0]?.message ?? "not an absolute URI"}`
|
|
332
505
|
};
|
|
333
506
|
return { ok: true };
|
|
334
507
|
}
|
|
@@ -654,10 +827,10 @@ function invalidateResourceCache(identifier) {
|
|
|
654
827
|
* Looks up an {@link OAuthResource} by `identifier`, consulting the in-process
|
|
655
828
|
* cache when the identifier is in {@link OAuthOptions.cachedResources}.
|
|
656
829
|
*
|
|
657
|
-
* Triggers the lazy seed via {@link seedResourcesOnce} on first call —
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
830
|
+
* Triggers the lazy seed via {@link seedResourcesOnce} on first call — the
|
|
831
|
+
* retry path when the startup seed failed (database unreachable, or
|
|
832
|
+
* migrations run after plugin init). The seed is idempotent and coalesced,
|
|
833
|
+
* so the cost is paid only once per process.
|
|
661
834
|
*
|
|
662
835
|
* Returns a defensive copy so callers can't mutate cached state.
|
|
663
836
|
*
|
|
@@ -748,29 +921,56 @@ function buildSeedUpdate(input, mode, now) {
|
|
|
748
921
|
}
|
|
749
922
|
/**
|
|
750
923
|
* Pattern matched against adapter errors to detect the "table not yet
|
|
751
|
-
* created" case — i.e. migrations haven't
|
|
752
|
-
*
|
|
924
|
+
* created" case — i.e. migrations or `auth generate` haven't run. Only
|
|
925
|
+
* used to keep the expected startup seed failure quiet.
|
|
753
926
|
*
|
|
754
927
|
* Covers SQLite ("no such table"), Postgres ("relation X does not exist"),
|
|
755
|
-
*
|
|
928
|
+
* MySQL ("Table X does not exist" / contracted form), and the Drizzle and
|
|
929
|
+
* Prisma adapters' missing-model errors.
|
|
930
|
+
*/
|
|
931
|
+
const MISSING_TABLE_PATTERN = /no such table|relation.*does not exist|table.*does(?: not|n'?t) exist|model.*(?:does not exist|was not found in the schema)/i;
|
|
932
|
+
/**
|
|
933
|
+
* Logs a failed startup seed. Missing tables are expected before migrations
|
|
934
|
+
* and stay quiet; anything else is an error worth seeing.
|
|
935
|
+
*
|
|
936
|
+
* @internal
|
|
756
937
|
*/
|
|
757
|
-
|
|
938
|
+
function logStartupSeedFailure(ctx, error) {
|
|
939
|
+
let missingTable = false;
|
|
940
|
+
let current = error;
|
|
941
|
+
for (let depth = 0; current && !missingTable && depth < 5; depth++) {
|
|
942
|
+
missingTable = MISSING_TABLE_PATTERN.test(current instanceof Error ? current.message : String(current));
|
|
943
|
+
current = current instanceof Error ? current.cause : void 0;
|
|
944
|
+
}
|
|
945
|
+
if (missingTable) {
|
|
946
|
+
ctx.logger.debug("oauth-provider: oauthResource table not yet created; deferring resource seed to first access.");
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
ctx.logger.error("oauth-provider: could not seed `resources` at startup; retrying on first resource access.", error);
|
|
950
|
+
}
|
|
758
951
|
/**
|
|
759
|
-
*
|
|
760
|
-
* one Better Auth instance suppress seeding for every later instance in the
|
|
761
|
-
* process. Endpoint `AuthContext` objects are not guaranteed to be stable
|
|
762
|
-
* requests, so
|
|
952
|
+
* Lazy-seed state per adapter and plugin options. A module-level boolean would
|
|
953
|
+
* let one Better Auth instance suppress seeding for every later instance in the
|
|
954
|
+
* same process. Endpoint `AuthContext` objects are not guaranteed to be stable
|
|
955
|
+
* across requests, so the adapter identifies the backing store, and the options
|
|
956
|
+
* object separates instances that share an adapter but configure different
|
|
957
|
+
* `resources`.
|
|
763
958
|
*/
|
|
764
959
|
let seedStates = /* @__PURE__ */ new WeakMap();
|
|
765
|
-
function getSeedState(ctx) {
|
|
766
|
-
const
|
|
767
|
-
|
|
960
|
+
function getSeedState(ctx, opts) {
|
|
961
|
+
const adapterKey = ctx.adapter;
|
|
962
|
+
let byOptions = seedStates.get(adapterKey);
|
|
963
|
+
if (!byOptions) {
|
|
964
|
+
byOptions = /* @__PURE__ */ new WeakMap();
|
|
965
|
+
seedStates.set(adapterKey, byOptions);
|
|
966
|
+
}
|
|
967
|
+
const existing = byOptions.get(opts);
|
|
768
968
|
if (existing) return existing;
|
|
769
969
|
const created = {
|
|
770
970
|
completed: false,
|
|
771
971
|
promise: null
|
|
772
972
|
};
|
|
773
|
-
|
|
973
|
+
byOptions.set(opts, created);
|
|
774
974
|
return created;
|
|
775
975
|
}
|
|
776
976
|
/**
|
|
@@ -790,13 +990,11 @@ function getSeedState(ctx) {
|
|
|
790
990
|
*
|
|
791
991
|
* Race-safety: the `identifier` column carries a UNIQUE constraint, so two
|
|
792
992
|
* processes booting simultaneously can each attempt the insert — one wins,
|
|
793
|
-
* the other
|
|
993
|
+
* the other re-reads the row and treats the failed insert as a no-op.
|
|
794
994
|
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
* after
|
|
798
|
-
* defers — the lazy {@link seedResourcesOnce} path picks up the work on
|
|
799
|
-
* the first resource access.
|
|
995
|
+
* Throws on storage errors, including a missing table. Callers go through
|
|
996
|
+
* {@link seedResourcesOnce}, which resets so the next resource access
|
|
997
|
+
* retries (e.g. after migrations run).
|
|
800
998
|
*
|
|
801
999
|
* Idempotent: safe to call multiple times.
|
|
802
1000
|
*
|
|
@@ -819,69 +1017,45 @@ async function seedResources(ctx, opts) {
|
|
|
819
1017
|
const { signingAlgorithm: _, ...rest } = input;
|
|
820
1018
|
input = rest;
|
|
821
1019
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1020
|
+
const where = [{
|
|
1021
|
+
field: "identifier",
|
|
1022
|
+
value: input.identifier
|
|
1023
|
+
}];
|
|
1024
|
+
const existing = await ctx.adapter.findOne({
|
|
1025
|
+
model: modelName,
|
|
1026
|
+
where
|
|
1027
|
+
});
|
|
1028
|
+
const now = /* @__PURE__ */ new Date();
|
|
1029
|
+
if (!existing) try {
|
|
1030
|
+
await ctx.adapter.create({
|
|
825
1031
|
model: modelName,
|
|
826
|
-
|
|
827
|
-
field: "identifier",
|
|
828
|
-
value: input.identifier
|
|
829
|
-
}]
|
|
1032
|
+
data: buildSeedRow(input, now)
|
|
830
1033
|
});
|
|
831
|
-
} catch (err) {
|
|
832
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
833
|
-
if (MISSING_TABLE_PATTERN.test(message)) {
|
|
834
|
-
logger.debug("oauth-provider: oauthResource table not yet created; deferring resource seed to first access.");
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
throw err;
|
|
838
|
-
}
|
|
839
|
-
const now = /* @__PURE__ */ new Date();
|
|
840
|
-
if (!existing) {
|
|
841
|
-
try {
|
|
842
|
-
await ctx.adapter.create({
|
|
843
|
-
model: modelName,
|
|
844
|
-
data: buildSeedRow(input, now)
|
|
845
|
-
});
|
|
846
|
-
} catch (err) {
|
|
847
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
848
|
-
if (/unique|duplicate|UNIQUE/i.test(message)) {
|
|
849
|
-
logger.debug(`oauth-provider: resource ${input.identifier} already inserted by a concurrent process — skipping.`);
|
|
850
|
-
continue;
|
|
851
|
-
}
|
|
852
|
-
if (MISSING_TABLE_PATTERN.test(message)) {
|
|
853
|
-
logger.debug("oauth-provider: oauthResource table not yet created; deferring resource seed to first access.");
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
throw err;
|
|
857
|
-
}
|
|
858
1034
|
continue;
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
if (!await ctx.adapter.findOne({
|
|
1037
|
+
model: modelName,
|
|
1038
|
+
where
|
|
1039
|
+
}).catch(() => null)) throw err;
|
|
859
1040
|
}
|
|
860
1041
|
if (mode === "insertOnly") continue;
|
|
861
1042
|
await ctx.adapter.update({
|
|
862
1043
|
model: modelName,
|
|
863
|
-
where
|
|
864
|
-
field: "identifier",
|
|
865
|
-
value: input.identifier
|
|
866
|
-
}],
|
|
1044
|
+
where,
|
|
867
1045
|
update: buildSeedUpdate(input, mode, now)
|
|
868
1046
|
});
|
|
869
1047
|
}
|
|
870
1048
|
}
|
|
871
1049
|
/**
|
|
872
|
-
* Idempotent, coalesced wrapper around {@link seedResources}
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
* The endpoint context shape (`{ context: AuthContext }`) is what
|
|
878
|
-
* `getResource` receives, so this is a convenience overload that unwraps
|
|
879
|
-
* for callers.
|
|
1050
|
+
* Idempotent, coalesced wrapper around {@link seedResources}, called from
|
|
1051
|
+
* plugin `init` and from every resource lookup. The first call runs the
|
|
1052
|
+
* seed, concurrent calls await the same promise, and calls after a success
|
|
1053
|
+
* are no-ops. A failure resets the state so the next call retries.
|
|
880
1054
|
*
|
|
881
1055
|
* @internal
|
|
882
1056
|
*/
|
|
883
1057
|
async function seedResourcesOnce(ctx, opts) {
|
|
884
|
-
const state = getSeedState(ctx);
|
|
1058
|
+
const state = getSeedState(ctx, opts);
|
|
885
1059
|
if (state.completed === true) return;
|
|
886
1060
|
if (state.promise !== null) return state.promise;
|
|
887
1061
|
state.promise = seedResources(ctx, opts).then(() => {
|
|
@@ -909,8 +1083,15 @@ function logEnforcePerClientResourcesResolution(opts) {
|
|
|
909
1083
|
function getDpopProofJwt(ctx) {
|
|
910
1084
|
return ctx.headers?.get("dpop") ?? void 0;
|
|
911
1085
|
}
|
|
1086
|
+
/**
|
|
1087
|
+
* The endpoint's public URL, built from `baseURL`, which clients use as the
|
|
1088
|
+
* DPoP `htu`. Not `request.url`: proxies and dev servers can rewrite its host.
|
|
1089
|
+
*/
|
|
912
1090
|
function getEndpointUrl(ctx, path) {
|
|
913
|
-
|
|
1091
|
+
const { baseURL } = ctx.context;
|
|
1092
|
+
let end = baseURL.length;
|
|
1093
|
+
while (end > 0 && baseURL[end - 1] === "/") end--;
|
|
1094
|
+
return `${baseURL.slice(0, end)}${path}`;
|
|
914
1095
|
}
|
|
915
1096
|
//#endregion
|
|
916
1097
|
//#region src/oauthClient/client-credentials.ts
|
|
@@ -944,187 +1125,6 @@ function validateClientCredentialsScopes(scopes, grantTypes, tokenEndpointAuthMe
|
|
|
944
1125
|
});
|
|
945
1126
|
}
|
|
946
1127
|
//#endregion
|
|
947
|
-
//#region src/types/zod.ts
|
|
948
|
-
const DANGEROUS_SCHEMES = [
|
|
949
|
-
"javascript:",
|
|
950
|
-
"data:",
|
|
951
|
-
"vbscript:"
|
|
952
|
-
];
|
|
953
|
-
/**
|
|
954
|
-
* Validates an RFC 8707 resource indicator. The value must be an absolute URI
|
|
955
|
-
* with no fragment (RFC 8707 §2). Unlike a redirect URI it is not restricted to
|
|
956
|
-
* HTTPS, because a resource server identifier may use any absolute URI scheme;
|
|
957
|
-
* configured OAuth resources are the authoritative control over
|
|
958
|
-
* which resources a token may target.
|
|
959
|
-
*/
|
|
960
|
-
const ResourceUriSchema = z.string().superRefine((val, ctx) => {
|
|
961
|
-
if (!URL.canParse(val)) {
|
|
962
|
-
ctx.addIssue({
|
|
963
|
-
code: "custom",
|
|
964
|
-
message: "resource must be an absolute URI",
|
|
965
|
-
fatal: true
|
|
966
|
-
});
|
|
967
|
-
return z.NEVER;
|
|
968
|
-
}
|
|
969
|
-
if (val.includes("#")) {
|
|
970
|
-
ctx.addIssue({
|
|
971
|
-
code: "custom",
|
|
972
|
-
message: "resource must not contain a fragment"
|
|
973
|
-
});
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
if (DANGEROUS_SCHEMES.includes(new URL(val).protocol)) ctx.addIssue({
|
|
977
|
-
code: "custom",
|
|
978
|
-
message: "resource cannot use javascript:, data:, or vbscript: scheme"
|
|
979
|
-
});
|
|
980
|
-
});
|
|
981
|
-
const authorizationPromptTokenSchema = z.enum([
|
|
982
|
-
"none",
|
|
983
|
-
"consent",
|
|
984
|
-
"login",
|
|
985
|
-
"create",
|
|
986
|
-
"select_account"
|
|
987
|
-
]);
|
|
988
|
-
const authorizationPromptSchema = z.string().superRefine((value, ctx) => {
|
|
989
|
-
const promptTokens = value.split(" ").map((token) => token.trim()).filter(Boolean);
|
|
990
|
-
const promptSet = /* @__PURE__ */ new Set();
|
|
991
|
-
if (!promptTokens.length) {
|
|
992
|
-
ctx.addIssue({
|
|
993
|
-
code: "custom",
|
|
994
|
-
message: "prompt must include at least one value"
|
|
995
|
-
});
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
for (const token of promptTokens) {
|
|
999
|
-
const result = authorizationPromptTokenSchema.safeParse(token);
|
|
1000
|
-
if (!result.success) {
|
|
1001
|
-
ctx.addIssue({
|
|
1002
|
-
code: "custom",
|
|
1003
|
-
message: `unsupported prompt value: ${token}`
|
|
1004
|
-
});
|
|
1005
|
-
continue;
|
|
1006
|
-
}
|
|
1007
|
-
promptSet.add(result.data);
|
|
1008
|
-
}
|
|
1009
|
-
if (promptSet.has("none") && promptSet.size > 1) ctx.addIssue({
|
|
1010
|
-
code: "custom",
|
|
1011
|
-
message: "prompt=none cannot be combined with other prompt values"
|
|
1012
|
-
});
|
|
1013
|
-
});
|
|
1014
|
-
/** Canonical RFC 7517 JWK Set metadata for an OAuth client. */
|
|
1015
|
-
const clientJwksSchema = z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) });
|
|
1016
|
-
const maxAgeSchema = z.union([z.number(), z.string().trim().min(1)]).transform((value, ctx) => {
|
|
1017
|
-
const maxAge = typeof value === "number" ? value : Number(value);
|
|
1018
|
-
if (!Number.isInteger(maxAge) || maxAge < 0) {
|
|
1019
|
-
ctx.addIssue({
|
|
1020
|
-
code: "custom",
|
|
1021
|
-
message: "max_age must be a non-negative integer"
|
|
1022
|
-
});
|
|
1023
|
-
return z.NEVER;
|
|
1024
|
-
}
|
|
1025
|
-
return maxAge;
|
|
1026
|
-
});
|
|
1027
|
-
const dpopJktSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/, "dpop_jkt must be a base64url-encoded SHA-256 JWK thumbprint");
|
|
1028
|
-
/**
|
|
1029
|
-
* Runtime schema for OAuthAuthorizationQuery.
|
|
1030
|
-
* Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
|
|
1031
|
-
*/
|
|
1032
|
-
const authorizationQuerySchema = z.object({
|
|
1033
|
-
response_type: z.string().pipe(z.enum(["code"])).optional(),
|
|
1034
|
-
request: z.string().optional(),
|
|
1035
|
-
request_uri: z.string().optional(),
|
|
1036
|
-
redirect_uri: SafeUrlSchema.optional(),
|
|
1037
|
-
scope: z.string().optional(),
|
|
1038
|
-
state: z.string().optional(),
|
|
1039
|
-
client_id: z.string().min(1, "client_id is required"),
|
|
1040
|
-
prompt: authorizationPromptSchema.optional(),
|
|
1041
|
-
display: z.string().optional(),
|
|
1042
|
-
ui_locales: z.string().optional(),
|
|
1043
|
-
max_age: maxAgeSchema.optional(),
|
|
1044
|
-
acr_values: z.string().optional(),
|
|
1045
|
-
login_hint: z.string().optional(),
|
|
1046
|
-
id_token_hint: z.string().optional(),
|
|
1047
|
-
code_challenge: z.string().optional(),
|
|
1048
|
-
code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
|
|
1049
|
-
nonce: z.string().optional(),
|
|
1050
|
-
claims: claimsRequestInputSchema.optional(),
|
|
1051
|
-
dpop_jkt: dpopJktSchema.optional(),
|
|
1052
|
-
resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional()
|
|
1053
|
-
}).passthrough();
|
|
1054
|
-
const storedAuthorizationQuerySchema = authorizationQuerySchema.extend({
|
|
1055
|
-
redirect_uri: SafeUrlSchema.optional(),
|
|
1056
|
-
claims: claimsRequestParameterSchema.optional()
|
|
1057
|
-
});
|
|
1058
|
-
/**
|
|
1059
|
-
* Runtime schema for the authorization code verification value.
|
|
1060
|
-
* Validates structure on deserialization from the JSON blob stored in the DB.
|
|
1061
|
-
* Uses passthrough so future fields (e.g. from authorization challenge) don't break parsing.
|
|
1062
|
-
*/
|
|
1063
|
-
const verificationValueSchema = z.object({
|
|
1064
|
-
type: z.literal("authorization_code"),
|
|
1065
|
-
query: storedAuthorizationQuerySchema,
|
|
1066
|
-
sessionId: z.string(),
|
|
1067
|
-
userId: z.string(),
|
|
1068
|
-
referenceId: z.string().optional(),
|
|
1069
|
-
authTime: z.number().optional(),
|
|
1070
|
-
resource: z.array(z.string()).optional()
|
|
1071
|
-
}).passthrough();
|
|
1072
|
-
/**
|
|
1073
|
-
* Request body accepted at `POST /oauth2/register` (RFC 7591 §2 client
|
|
1074
|
-
* metadata). This is the single source of truth for the registration contract:
|
|
1075
|
-
* the endpoint validates against it and {@link ClientRegistrationRequest} is
|
|
1076
|
-
* inferred from it, so the type a `validateInitialAccessToken` callback receives
|
|
1077
|
-
* always matches what is actually validated. `grant_types` and
|
|
1078
|
-
* `token_endpoint_auth_method` are open strings because extensions can register
|
|
1079
|
-
* custom values. Server-assigned fields (`client_id`, `client_secret`, the
|
|
1080
|
-
* issued/expiry timestamps) and internal state (`disabled`, `reference_id`) are
|
|
1081
|
-
* never part of a registration request.
|
|
1082
|
-
*
|
|
1083
|
-
* @see https://datatracker.ietf.org/doc/html/rfc7591#section-2
|
|
1084
|
-
*/
|
|
1085
|
-
const clientRegistrationRequestSchema = z.object({
|
|
1086
|
-
redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
1087
|
-
scope: z.string().optional(),
|
|
1088
|
-
client_name: z.string().refine((value) => value.trim().length > 0, "client_name cannot be empty").optional(),
|
|
1089
|
-
client_uri: z.string().optional(),
|
|
1090
|
-
logo_uri: z.string().optional(),
|
|
1091
|
-
contacts: z.array(z.string().min(1)).min(1).optional(),
|
|
1092
|
-
tos_uri: z.string().optional(),
|
|
1093
|
-
policy_uri: z.string().optional(),
|
|
1094
|
-
software_id: z.string().optional(),
|
|
1095
|
-
software_version: z.string().optional(),
|
|
1096
|
-
software_statement: z.string().optional(),
|
|
1097
|
-
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
1098
|
-
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
1099
|
-
backchannel_logout_session_required: z.boolean().optional(),
|
|
1100
|
-
token_endpoint_auth_method: z.string().trim().min(1).optional(),
|
|
1101
|
-
jwks: clientJwksSchema.optional(),
|
|
1102
|
-
jwks_uri: z.string().optional(),
|
|
1103
|
-
grant_types: z.array(z.string().trim().min(1)).min(1).optional(),
|
|
1104
|
-
response_types: z.array(z.enum(["code"])).optional(),
|
|
1105
|
-
application_type: z.enum(["web", "native"]).optional(),
|
|
1106
|
-
subject_type: z.enum(["public", "pairwise"]).optional(),
|
|
1107
|
-
dpop_bound_access_tokens: z.boolean().optional(),
|
|
1108
|
-
resources: z.array(ResourceUriSchema).optional(),
|
|
1109
|
-
skip_consent: z.never({ error: "skip_consent cannot be set during dynamic client registration" }).optional()
|
|
1110
|
-
});
|
|
1111
|
-
/**
|
|
1112
|
-
* Complete OAuth client metadata document schema shared by registration and
|
|
1113
|
-
* verified client-discovery integrations.
|
|
1114
|
-
*
|
|
1115
|
-
* Unlike {@link clientRegistrationRequestSchema}, a metadata document carries
|
|
1116
|
-
* its own identifier and may contain the server-assigned secret fields that a
|
|
1117
|
-
* discovery profile must explicitly prohibit. Keeping those fields in the
|
|
1118
|
-
* parsed shape lets profile validation distinguish a forbidden value from an
|
|
1119
|
-
* unknown extension without unsafe casts or ad-hoc allowlists.
|
|
1120
|
-
*/
|
|
1121
|
-
const oauthClientMetadataSchema = clientRegistrationRequestSchema.omit({ skip_consent: true }).extend({
|
|
1122
|
-
client_id: z.string().refine((value) => value.trim().length > 0, "client_id cannot be empty"),
|
|
1123
|
-
client_secret: z.string().optional(),
|
|
1124
|
-
client_secret_expires_at: z.number().int().nonnegative().optional(),
|
|
1125
|
-
client_id_issued_at: z.number().int().nonnegative().optional()
|
|
1126
|
-
}).loose();
|
|
1127
|
-
//#endregion
|
|
1128
1128
|
//#region src/userinfo.ts
|
|
1129
1129
|
/**
|
|
1130
1130
|
* Builds the standard OIDC claims (OIDC Core §5.1) for the UserInfo response.
|
|
@@ -1645,7 +1645,7 @@ async function resolveDpopTokenBinding(ctx, opts, params) {
|
|
|
1645
1645
|
return { jkt: (await verifyDpopProof({
|
|
1646
1646
|
proofJwt: dpopProofJwt,
|
|
1647
1647
|
method: "POST",
|
|
1648
|
-
url: getEndpointUrl(ctx, "/oauth2/token"),
|
|
1648
|
+
url: getEndpointUrl(ctx, ctx.path ?? "/oauth2/token"),
|
|
1649
1649
|
expectedJkt,
|
|
1650
1650
|
proofMaxAgeSeconds: opts.dpop?.proofMaxAgeSeconds,
|
|
1651
1651
|
signingAlgorithms: opts.dpop?.signingAlgorithms,
|
|
@@ -2548,4 +2548,4 @@ async function introspectEndpoint(ctx, opts) {
|
|
|
2548
2548
|
}
|
|
2549
2549
|
}
|
|
2550
2550
|
//#endregion
|
|
2551
|
-
export {
|
|
2551
|
+
export { claimsRequestParameterSchema as A, clientJwksSchema as C, STANDARD_CLAIM_NAMES as D, STANDARD_CLAIMS as E, getRequestedUserInfoClaims as M, isValidOidcClaimsRequest as N, getSupportedClaims as O, authorizationQuerySchema as S, oauthClientMetadataSchema as T, logStartupSeedFailure as _, invalidateRefreshFamily as a, ResourceUriSchema as b, normalizeClientCredentialsScopes as c, assertIdentifierValid as d, extractRepeatedResourceFromForm as f, logEnforcePerClientResourcesResolution as g, isAudienceClaimAllowed as h, getOAuthProviderApi as i, filterClaimsRequestUserInfoClaims as j, canSatisfyEssentialAcrRequest as k, validateClientCredentialsScopes as l, invalidateResourceCache as m, introspect_exports as n, tokenEndpoint as o, getResource as p, decodeRefreshToken as r, userInfoEndpoint as s, introspectEndpoint as t, JWS_ALGORITHMS as u, resolveResourcePolicy as v, clientRegistrationRequestSchema as w, SafeUrlSchema as x, seedResourcesOnce as y };
|
|
@@ -1,4 +1,4 @@
|
|
|
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-DAMtT2vg.mjs";
|
|
2
2
|
import * as _$better_call0 from "better-call";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import * as _$jose from "jose";
|
|
@@ -1785,6 +1785,7 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
1785
1785
|
jwks_uri: z.ZodOptional<z.ZodString>;
|
|
1786
1786
|
grant_types: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1787
1787
|
client_credentials_scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1788
|
+
resources: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1788
1789
|
response_types: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
1789
1790
|
code: "code";
|
|
1790
1791
|
}>>>;
|
|
@@ -2280,6 +2281,7 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
2280
2281
|
client_secret_expires_at: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
2281
2282
|
skip_consent: z.ZodOptional<z.ZodBoolean>;
|
|
2282
2283
|
enable_end_session: z.ZodOptional<z.ZodBoolean>;
|
|
2284
|
+
disabled: z.ZodOptional<z.ZodBoolean>;
|
|
2283
2285
|
dpop_bound_access_tokens: z.ZodOptional<z.ZodBoolean>;
|
|
2284
2286
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
2285
2287
|
}, z.core.$strip>;
|
|
@@ -1429,6 +1429,27 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
|
|
|
1429
1429
|
* @default true
|
|
1430
1430
|
*/
|
|
1431
1431
|
clientRegistrationRequirePKCE?: boolean;
|
|
1432
|
+
/**
|
|
1433
|
+
* Default `application_type` for dynamic client registrations that omit the
|
|
1434
|
+
* field.
|
|
1435
|
+
*
|
|
1436
|
+
* - `"web"`: the OIDC Registration §2 default. Omitted `application_type` is
|
|
1437
|
+
* treated as `web`, so custom-scheme redirect URIs are rejected.
|
|
1438
|
+
* - `"native"`: omitted `application_type` is treated as `native`.
|
|
1439
|
+
* - `"infer"`: omitted `application_type` is treated as `native` when any
|
|
1440
|
+
* `redirect_uris` entry uses a non-http(s) scheme; otherwise `web`. Every
|
|
1441
|
+
* redirect URI is then validated against the native rules. Lets native MCP
|
|
1442
|
+
* clients that omit `application_type` (for example Cursor, which also
|
|
1443
|
+
* registers https and loopback callbacks) register without loosening
|
|
1444
|
+
* web-client redirect policy.
|
|
1445
|
+
*
|
|
1446
|
+
* An `application_type` the client actually sends is never overridden.
|
|
1447
|
+
* Applies only to dynamic client registration; managed clients and Client ID
|
|
1448
|
+
* Metadata Documents are unaffected.
|
|
1449
|
+
*
|
|
1450
|
+
* @default "web"
|
|
1451
|
+
*/
|
|
1452
|
+
clientRegistrationDefaultApplicationType?: "web" | "native" | "infer";
|
|
1432
1453
|
/**
|
|
1433
1454
|
* How long a dynamically created confidential client
|
|
1434
1455
|
* should last for.
|
|
@@ -1480,6 +1501,16 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
|
|
|
1480
1501
|
action: "create" | "read" | "update" | "delete" | "list" | "rotate" | "configure-client-credentials-scopes";
|
|
1481
1502
|
user?: User & Record<string, unknown>;
|
|
1482
1503
|
session?: Session & Record<string, unknown>;
|
|
1504
|
+
/**
|
|
1505
|
+
* The `client_id` from the request, for actions on an existing client.
|
|
1506
|
+
* Absent for `create`, `list`, and scope configuration during creation.
|
|
1507
|
+
* The client may not exist, and this value does not establish ownership.
|
|
1508
|
+
* Client CRUD enforces ownership except when `adminUpdateOAuthClient`
|
|
1509
|
+
* changes only `client_credentials_scopes`: the `update` and
|
|
1510
|
+
* `configure-client-credentials-scopes` decisions can authorize access
|
|
1511
|
+
* to another owner's client.
|
|
1512
|
+
*/
|
|
1513
|
+
clientId?: string;
|
|
1483
1514
|
}) => Awaitable<boolean | undefined>;
|
|
1484
1515
|
/**
|
|
1485
1516
|
* Grant types supported by the token endpoint
|
|
@@ -1611,7 +1642,8 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
|
|
|
1611
1642
|
* For example, allow selection of an organization or team.
|
|
1612
1643
|
*
|
|
1613
1644
|
* Upon selection of a specific account, use `/oauth2/continue`
|
|
1614
|
-
* with `postLogin: true` to continue the login flow.
|
|
1645
|
+
* with `postLogin: true` to continue the login flow. The continue
|
|
1646
|
+
* request runs `shouldRedirect` again with `isContinuation: true`.
|
|
1615
1647
|
*
|
|
1616
1648
|
* @returns
|
|
1617
1649
|
* - `true`: account is not selected and needs selection
|
|
@@ -1622,6 +1654,12 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
|
|
|
1622
1654
|
user: User & Record<string, unknown>;
|
|
1623
1655
|
session: Session & Record<string, unknown>;
|
|
1624
1656
|
scopes: Scopes;
|
|
1657
|
+
/**
|
|
1658
|
+
* `true` when the user returns from `page` through `/oauth2/continue`.
|
|
1659
|
+
* The client chooses to send that request, so it does not prove a
|
|
1660
|
+
* selection was made: validate the session before returning `false`.
|
|
1661
|
+
*/
|
|
1662
|
+
isContinuation: boolean;
|
|
1625
1663
|
}) => Awaitable<boolean>;
|
|
1626
1664
|
};
|
|
1627
1665
|
/**
|
|
@@ -712,7 +712,7 @@ async function extractClientCredentials(ctx, opts, expectedAudience) {
|
|
|
712
712
|
confirmation: result.confirmation
|
|
713
713
|
};
|
|
714
714
|
}
|
|
715
|
-
const { verifyClientAssertion: verify } = await import("./authorize-
|
|
715
|
+
const { verifyClientAssertion: verify } = await import("./authorize-D8r6mvZx.mjs").then((n) => n.o);
|
|
716
716
|
return {
|
|
717
717
|
kind: "pre_verified",
|
|
718
718
|
method: "private_key_jwt",
|
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.9",
|
|
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.9",
|
|
75
|
+
"@better-auth/memory-adapter": "1.7.9",
|
|
76
|
+
"better-auth": "1.7.9"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@better-auth/utils": "0.4.2",
|
|
79
|
+
"@better-auth/utils": "^0.4.2 || ^0.5.0",
|
|
80
80
|
"@better-fetch/fetch": "1.3.2",
|
|
81
|
-
"better-call": "1.4.
|
|
82
|
-
"@better-auth/core": "^1.7.
|
|
83
|
-
"better-auth": "^1.7.
|
|
81
|
+
"better-call": "1.4.2",
|
|
82
|
+
"@better-auth/core": "^1.7.9",
|
|
83
|
+
"better-auth": "^1.7.9"
|
|
84
84
|
},
|
|
85
85
|
"scripts": {
|
|
86
86
|
"build": "tsdown",
|