@better-auth/oauth-provider 1.7.0-beta.1 → 1.7.0-beta.2
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/client-resource.mjs +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +242 -92
- package/dist/{oauth-B_qonG53.d.mts → oauth-DJcZ8MMZ.d.mts} +59 -19
- package/dist/{version-DIwdpXrQ.mjs → version-CZxZ64qJ.mjs} +1 -1
- package/package.json +5 -5
package/dist/client-resource.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as handleMcpErrors } from "./mcp-CYnz-MXn.mjs";
|
|
2
2
|
import { o as getJwtPlugin, s as getOAuthProviderPlugin } from "./utils-Cx_XnD9i.mjs";
|
|
3
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
3
|
+
import { t as PACKAGE_VERSION } from "./version-CZxZ64qJ.mjs";
|
|
4
4
|
import { verifyAccessToken } from "better-auth/oauth2";
|
|
5
5
|
import { APIError } from "better-call";
|
|
6
6
|
import { logger } from "@better-auth/core/env";
|
package/dist/client.d.mts
CHANGED
package/dist/client.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _ as Scope, a as OIDCMetadata, b as Awaitable, c as AuthorizePrompt, d as OAuthConsent, f as OAuthOpaqueAccessToken, g as SchemaClient, h as Prompt, i as OAuthClient, l as ClientDiscovery, m as OAuthRefreshToken, n as AuthServerMetadata, o as ResourceServerMetadata, p as OAuthOptions, r as GrantType, s as TokenEndpointAuthMethod, t as AuthMethod, u as OAuthAuthorizationQuery, v as StoreTokenType, y as VerificationValue } from "./oauth-CU79t-eG.mjs";
|
|
2
|
-
import { n as oauthProvider, t as getOAuthProviderState } from "./oauth-
|
|
2
|
+
import { a as OAuthErrorCode, c as OAuthRedirectOnError, i as OAuthEndpointRedirectContext, n as oauthProvider, o as OAuthFieldErrorCode, r as OAuthEndpointErrorResult, s as OAuthFieldErrorCodeMap, t as getOAuthProviderState } from "./oauth-DJcZ8MMZ.mjs";
|
|
3
3
|
import { verifyAccessToken } from "better-auth/oauth2";
|
|
4
4
|
import { JWSAlgorithms, JwtOptions } from "better-auth/plugins";
|
|
5
5
|
import { JWTPayload } from "jose";
|
|
@@ -105,4 +105,4 @@ declare function checkOAuthClient(client: OAuthClient, opts: OAuthOptions<Scope[
|
|
|
105
105
|
*/
|
|
106
106
|
declare function oauthToSchema(input: OAuthClient): SchemaClient<Scope[]>;
|
|
107
107
|
//#endregion
|
|
108
|
-
export { AuthServerMetadata, AuthorizePrompt, ClientDiscovery, OAuthAuthorizationQuery, OAuthClient, OAuthConsent, OAuthOpaqueAccessToken, OAuthOptions, OAuthRefreshToken, OIDCMetadata, Prompt, ResourceServerMetadata, SchemaClient, Scope, StoreTokenType, VerificationValue, authServerMetadata, checkOAuthClient, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata };
|
|
108
|
+
export { AuthServerMetadata, AuthorizePrompt, ClientDiscovery, OAuthAuthorizationQuery, OAuthClient, OAuthConsent, type OAuthEndpointErrorResult, type OAuthEndpointRedirectContext, type OAuthErrorCode, type OAuthFieldErrorCode, type OAuthFieldErrorCodeMap, OAuthOpaqueAccessToken, OAuthOptions, type OAuthRedirectOnError, OAuthRefreshToken, OIDCMetadata, Prompt, ResourceServerMetadata, SchemaClient, Scope, StoreTokenType, VerificationValue, authServerMetadata, checkOAuthClient, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata };
|
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { n as isPrivateHostname } from "./client-assertion-CderPEmR.mjs";
|
|
2
2
|
import { n as mcpHandler } from "./mcp-CYnz-MXn.mjs";
|
|
3
3
|
import { _ as storeClientSecret, a as getClient, b as validateClientCredentials, c as getStoredToken, d as normalizeTimestampValue, f as parseClientMetadata, g as searchParamsToQuery, h as resolveSubjectIdentifier, i as extractClientCredentials, l as isPKCERequired, m as resolveSessionAuthTime, n as deleteFromPrompt, o as getJwtPlugin, p as parsePrompt, r as destructureCredentials, t as decryptStoredClientSecret, u as mergeDiscoveryMetadata, v as storeToken, x as verifyOAuthQueryParams, y as toClientDiscoveryArray } from "./utils-Cx_XnD9i.mjs";
|
|
4
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
4
|
+
import { t as PACKAGE_VERSION } from "./version-CZxZ64qJ.mjs";
|
|
5
5
|
import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
6
6
|
import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
|
|
7
7
|
import { APIError as APIError$1 } from "better-call";
|
|
8
8
|
import { ASSERTION_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
|
|
9
9
|
import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
|
|
10
|
+
import { isLoopbackHost, isLoopbackIP } from "@better-auth/core/utils/host";
|
|
10
11
|
import { generateRandomString, makeSignature } from "better-auth/crypto";
|
|
11
12
|
import { defineRequestState } from "@better-auth/core/context";
|
|
12
13
|
import { logger } from "@better-auth/core/env";
|
|
@@ -162,9 +163,6 @@ const DANGEROUS_SCHEMES = [
|
|
|
162
163
|
"data:",
|
|
163
164
|
"vbscript:"
|
|
164
165
|
];
|
|
165
|
-
function isLocalhost(hostname) {
|
|
166
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.endsWith(".localhost");
|
|
167
|
-
}
|
|
168
166
|
/**
|
|
169
167
|
* Runtime schema for OAuthAuthorizationQuery.
|
|
170
168
|
* Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
|
|
@@ -203,7 +201,7 @@ const verificationValueSchema = z.object({
|
|
|
203
201
|
/**
|
|
204
202
|
* Reusable URL validation for OAuth redirect URIs.
|
|
205
203
|
* - Blocks dangerous schemes (javascript:, data:, vbscript:)
|
|
206
|
-
* - For http/https: requires HTTPS (HTTP allowed only for localhost)
|
|
204
|
+
* - For http/https: requires HTTPS (HTTP allowed only for loopback hosts: 127.0.0.0/8, [::1], *.localhost per RFC 6761)
|
|
207
205
|
* - Allows custom schemes for mobile apps (e.g., myapp://callback)
|
|
208
206
|
*/
|
|
209
207
|
const SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
|
@@ -223,12 +221,10 @@ const SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
|
|
223
221
|
});
|
|
224
222
|
return;
|
|
225
223
|
}
|
|
226
|
-
if (u.protocol === "http:"
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
});
|
|
231
|
-
}
|
|
224
|
+
if (u.protocol === "http:" && !isLoopbackHost(u.host)) ctx.addIssue({
|
|
225
|
+
code: "custom",
|
|
226
|
+
message: "Redirect URI must use HTTPS (HTTP allowed only for loopback hosts)"
|
|
227
|
+
});
|
|
232
228
|
});
|
|
233
229
|
//#endregion
|
|
234
230
|
//#region src/userinfo.ts
|
|
@@ -259,11 +255,7 @@ function userNormalClaims(user, scopes) {
|
|
|
259
255
|
* Handles the /oauth2/userinfo endpoint
|
|
260
256
|
*/
|
|
261
257
|
async function userInfoEndpoint(ctx, opts) {
|
|
262
|
-
|
|
263
|
-
error_description: "request not found",
|
|
264
|
-
error: "invalid_request"
|
|
265
|
-
});
|
|
266
|
-
const authorization = ctx.request.headers.get("authorization");
|
|
258
|
+
const authorization = ctx.headers?.get("authorization");
|
|
267
259
|
const token = typeof authorization === "string" && authorization?.startsWith("Bearer ") ? authorization?.replace("Bearer ", "") : authorization;
|
|
268
260
|
if (!token?.length) throw new APIError("UNAUTHORIZED", {
|
|
269
261
|
error_description: "authorization header not found",
|
|
@@ -309,8 +301,8 @@ async function userInfoEndpoint(ctx, opts) {
|
|
|
309
301
|
* the grant types
|
|
310
302
|
*/
|
|
311
303
|
async function tokenEndpoint(ctx, opts) {
|
|
312
|
-
const grantType = ctx.body
|
|
313
|
-
if (opts.grantTypes &&
|
|
304
|
+
const grantType = ctx.body.grant_type;
|
|
305
|
+
if (opts.grantTypes && !opts.grantTypes.includes(grantType)) throw new APIError("BAD_REQUEST", {
|
|
314
306
|
error_description: `unsupported grant_type ${grantType}`,
|
|
315
307
|
error: "unsupported_grant_type"
|
|
316
308
|
});
|
|
@@ -318,14 +310,6 @@ async function tokenEndpoint(ctx, opts) {
|
|
|
318
310
|
case "authorization_code": return handleAuthorizationCodeGrant(ctx, opts);
|
|
319
311
|
case "client_credentials": return handleClientCredentialsGrant(ctx, opts);
|
|
320
312
|
case "refresh_token": return handleRefreshTokenGrant(ctx, opts);
|
|
321
|
-
case void 0: throw new APIError("BAD_REQUEST", {
|
|
322
|
-
error_description: "missing required grant_type",
|
|
323
|
-
error: "unsupported_grant_type"
|
|
324
|
-
});
|
|
325
|
-
default: throw new APIError("BAD_REQUEST", {
|
|
326
|
-
error_description: `unsupported grant_type ${grantType}`,
|
|
327
|
-
error: "unsupported_grant_type"
|
|
328
|
-
});
|
|
329
313
|
}
|
|
330
314
|
}
|
|
331
315
|
async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, overrides) {
|
|
@@ -1016,6 +1000,7 @@ async function resolveIntrospectionSub(opts, payload, client) {
|
|
|
1016
1000
|
}
|
|
1017
1001
|
async function introspectEndpoint(ctx, opts) {
|
|
1018
1002
|
let { token, token_type_hint } = ctx.body;
|
|
1003
|
+
if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
|
|
1019
1004
|
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
|
|
1020
1005
|
if (!client_id || !client_secret && !preVerifiedClient) throw new APIError$1("UNAUTHORIZED", {
|
|
1021
1006
|
error_description: "missing required credentials",
|
|
@@ -1173,6 +1158,109 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
|
|
|
1173
1158
|
}
|
|
1174
1159
|
}
|
|
1175
1160
|
//#endregion
|
|
1161
|
+
//#region src/oauth-endpoint.ts
|
|
1162
|
+
/**
|
|
1163
|
+
* Wraps `createAuthEndpoint` so zod schemas stay the single source of truth
|
|
1164
|
+
* for body/query shape while validation failures serialize as the RFC 6749
|
|
1165
|
+
* §5.2 error envelope `{ error, error_description }`.
|
|
1166
|
+
*
|
|
1167
|
+
* A failing issue is routed by its first path segment via `errorCodesByField`:
|
|
1168
|
+
* - missing required (`invalid_type` + "received undefined") → `.missing`
|
|
1169
|
+
* - unsupported value (`invalid_value`) → `.invalid`
|
|
1170
|
+
* - anything else (wrong type, duplicated params, bad format) → `defaultError`
|
|
1171
|
+
*
|
|
1172
|
+
* For enum fields that need to distinguish missing from unsupported, compose
|
|
1173
|
+
* as `z.string().pipe(z.enum([...]))` so duplicated params fail the outer
|
|
1174
|
+
* `z.string()` as `invalid_type` instead of masquerading as an unsupported
|
|
1175
|
+
* enum value.
|
|
1176
|
+
*/
|
|
1177
|
+
function createOAuthEndpoint(path, options, handler) {
|
|
1178
|
+
const { redirectOnError, onValidationError: userHook, errorCodesByField, defaultError = "invalid_request", ...rest } = options;
|
|
1179
|
+
if (!redirectOnError) return createAuthEndpoint(path, {
|
|
1180
|
+
...rest,
|
|
1181
|
+
onValidationError: async (args) => {
|
|
1182
|
+
if (userHook) await userHook(args);
|
|
1183
|
+
throw new APIError$1("BAD_REQUEST", { ...mapIssuesToOAuthError(args.issues, errorCodesByField, defaultError) });
|
|
1184
|
+
}
|
|
1185
|
+
}, handler);
|
|
1186
|
+
const redirect = redirectOnError;
|
|
1187
|
+
const { body: bodySchema, query: querySchema, ...forwarded } = rest;
|
|
1188
|
+
async function validateSlot(ctx, slot, schema) {
|
|
1189
|
+
if (!schema) return { ok: true };
|
|
1190
|
+
const result = await schema.safeParseAsync(ctx[slot] ?? {});
|
|
1191
|
+
if (result.success) {
|
|
1192
|
+
ctx[slot] = result.data;
|
|
1193
|
+
return { ok: true };
|
|
1194
|
+
}
|
|
1195
|
+
if (userHook) await userHook({
|
|
1196
|
+
message: result.error.message,
|
|
1197
|
+
issues: result.error.issues
|
|
1198
|
+
});
|
|
1199
|
+
return {
|
|
1200
|
+
ok: false,
|
|
1201
|
+
response: redirect({
|
|
1202
|
+
...mapIssuesToOAuthError(result.error.issues, errorCodesByField, defaultError),
|
|
1203
|
+
ctx
|
|
1204
|
+
})
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
return createAuthEndpoint(path, forwarded, async (ctx) => {
|
|
1208
|
+
const body = await validateSlot(ctx, "body", bodySchema);
|
|
1209
|
+
if (!body.ok) return body.response;
|
|
1210
|
+
const query = await validateSlot(ctx, "query", querySchema);
|
|
1211
|
+
if (!query.ok) return query.response;
|
|
1212
|
+
return handler(ctx);
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
function mapIssuesToOAuthError(issues, errorCodesByField, defaultError = "invalid_request") {
|
|
1216
|
+
const issue = issues[0];
|
|
1217
|
+
if (!issue) return {
|
|
1218
|
+
error: defaultError,
|
|
1219
|
+
error_description: "Invalid request."
|
|
1220
|
+
};
|
|
1221
|
+
const first = issue.path?.[0];
|
|
1222
|
+
const fieldKey = typeof first === "string" ? first : void 0;
|
|
1223
|
+
const mapping = fieldKey ? errorCodesByField?.[fieldKey] : void 0;
|
|
1224
|
+
const field = issue.path?.length ? z.core.toDotPath(issue.path) : "";
|
|
1225
|
+
return {
|
|
1226
|
+
error: resolveErrorCode(issue, mapping, defaultError),
|
|
1227
|
+
error_description: describeIssue(issue, field)
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function resolveErrorCode(issue, mapping, defaultError) {
|
|
1231
|
+
if (typeof mapping === "string") return mapping;
|
|
1232
|
+
if (isMissingValueIssue(issue)) return mapping?.missing ?? defaultError;
|
|
1233
|
+
if (issue.code === "invalid_value") return mapping?.invalid ?? defaultError;
|
|
1234
|
+
return defaultError;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Returns `true` for issues that represent an absent required value. Zod v4
|
|
1238
|
+
* strips `input` from published issues, so the signal is the `invalid_type`
|
|
1239
|
+
* code combined with a message suffix of "received undefined". The suffix is
|
|
1240
|
+
* pinned by a regression test so a zod rephrase fails the test instead of
|
|
1241
|
+
* silently reclassifying missing fields.
|
|
1242
|
+
*
|
|
1243
|
+
* Assumes the default zod error map. Consumers that install a localized map
|
|
1244
|
+
* via `z.setErrorMap()` will break this check, collapsing missing-field
|
|
1245
|
+
* failures to `defaultError`.
|
|
1246
|
+
*/
|
|
1247
|
+
function isMissingValueIssue(issue) {
|
|
1248
|
+
return issue.code === "invalid_type" && issue.message.endsWith("received undefined");
|
|
1249
|
+
}
|
|
1250
|
+
function describeIssue(issue, field) {
|
|
1251
|
+
if (!field) return issue.message;
|
|
1252
|
+
if (issue.code === "invalid_type") {
|
|
1253
|
+
if (issue.message.endsWith("received undefined")) return `${field} is required`;
|
|
1254
|
+
if (issue.message.endsWith("received array")) return `${field} must not appear more than once`;
|
|
1255
|
+
return `${field} must be a ${issue.expected ?? "valid value"}`;
|
|
1256
|
+
}
|
|
1257
|
+
if (issue.code === "invalid_value") {
|
|
1258
|
+
const values = issue.values;
|
|
1259
|
+
if (Array.isArray(values) && values.length > 0) return `${field} must be one of: ${values.join(", ")}`;
|
|
1260
|
+
}
|
|
1261
|
+
return `${field}: ${issue.message}`;
|
|
1262
|
+
}
|
|
1263
|
+
//#endregion
|
|
1176
1264
|
//#region src/middleware/index.ts
|
|
1177
1265
|
const publicSessionMiddleware = (opts) => createAuthMiddleware(async (ctx) => {
|
|
1178
1266
|
if (!opts.allowPublicClientPrelogin) throw new APIError("BAD_REQUEST");
|
|
@@ -1458,14 +1546,8 @@ function schemaToOAuth(input) {
|
|
|
1458
1546
|
//#region src/oauthClient/endpoints.ts
|
|
1459
1547
|
async function getClientEndpoint(ctx, opts) {
|
|
1460
1548
|
const session = await getSessionFromCtx(ctx);
|
|
1549
|
+
await assertClientPrivileges(ctx, session, opts, "read");
|
|
1461
1550
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1462
|
-
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1463
|
-
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1464
|
-
headers: ctx.headers,
|
|
1465
|
-
action: "read",
|
|
1466
|
-
session: session.session,
|
|
1467
|
-
user: session.user
|
|
1468
|
-
})) throw new APIError("UNAUTHORIZED");
|
|
1469
1551
|
const client = await getClient(ctx, opts, ctx.query.client_id);
|
|
1470
1552
|
if (!client) throw new APIError("NOT_FOUND", {
|
|
1471
1553
|
error_description: "client not found",
|
|
@@ -1506,14 +1588,8 @@ async function getClientPublicEndpoint(ctx, opts, clientId) {
|
|
|
1506
1588
|
}
|
|
1507
1589
|
async function getClientsEndpoint(ctx, opts) {
|
|
1508
1590
|
const session = await getSessionFromCtx(ctx);
|
|
1591
|
+
await assertClientPrivileges(ctx, session, opts, "list");
|
|
1509
1592
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1510
|
-
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1511
|
-
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1512
|
-
headers: ctx.headers,
|
|
1513
|
-
action: "list",
|
|
1514
|
-
session: session.session,
|
|
1515
|
-
user: session.user
|
|
1516
|
-
})) throw new APIError("UNAUTHORIZED");
|
|
1517
1593
|
const referenceId = await opts.clientReference?.(session);
|
|
1518
1594
|
if (referenceId) return await ctx.context.adapter.findMany({
|
|
1519
1595
|
model: "oauthClient",
|
|
@@ -1547,14 +1623,8 @@ async function getClientsEndpoint(ctx, opts) {
|
|
|
1547
1623
|
}
|
|
1548
1624
|
async function deleteClientEndpoint(ctx, opts) {
|
|
1549
1625
|
const session = await getSessionFromCtx(ctx);
|
|
1626
|
+
await assertClientPrivileges(ctx, session, opts, "delete");
|
|
1550
1627
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1551
|
-
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1552
|
-
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1553
|
-
headers: ctx.headers,
|
|
1554
|
-
action: "delete",
|
|
1555
|
-
session: session.session,
|
|
1556
|
-
user: session.user
|
|
1557
|
-
})) throw new APIError("UNAUTHORIZED");
|
|
1558
1628
|
const clientId = ctx.body.client_id;
|
|
1559
1629
|
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1560
1630
|
error_description: "trusted clients must be updated manually",
|
|
@@ -1580,14 +1650,8 @@ async function deleteClientEndpoint(ctx, opts) {
|
|
|
1580
1650
|
}
|
|
1581
1651
|
async function updateClientEndpoint(ctx, opts) {
|
|
1582
1652
|
const session = await getSessionFromCtx(ctx);
|
|
1653
|
+
await assertClientPrivileges(ctx, session, opts, "update");
|
|
1583
1654
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1584
|
-
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1585
|
-
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1586
|
-
headers: ctx.headers,
|
|
1587
|
-
action: "update",
|
|
1588
|
-
session: session.session,
|
|
1589
|
-
user: session.user
|
|
1590
|
-
})) throw new APIError("UNAUTHORIZED");
|
|
1591
1655
|
const clientId = ctx.body.client_id;
|
|
1592
1656
|
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1593
1657
|
error_description: "trusted clients must be updated manually",
|
|
@@ -1641,14 +1705,8 @@ async function updateClientEndpoint(ctx, opts) {
|
|
|
1641
1705
|
}
|
|
1642
1706
|
async function rotateClientSecretEndpoint(ctx, opts) {
|
|
1643
1707
|
const session = await getSessionFromCtx(ctx);
|
|
1708
|
+
await assertClientPrivileges(ctx, session, opts, "rotate");
|
|
1644
1709
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1645
|
-
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1646
|
-
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1647
|
-
headers: ctx.headers,
|
|
1648
|
-
action: "rotate",
|
|
1649
|
-
session: session.session,
|
|
1650
|
-
user: session.user
|
|
1651
|
-
})) throw new APIError("UNAUTHORIZED");
|
|
1652
1710
|
const clientId = ctx.body.client_id;
|
|
1653
1711
|
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1654
1712
|
error_description: "trusted clients must be updated manually",
|
|
@@ -1690,6 +1748,16 @@ async function rotateClientSecretEndpoint(ctx, opts) {
|
|
|
1690
1748
|
clientSecret: (opts.prefix?.clientSecret ?? "") + clientSecret
|
|
1691
1749
|
});
|
|
1692
1750
|
}
|
|
1751
|
+
async function assertClientPrivileges(ctx, session, opts, action) {
|
|
1752
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1753
|
+
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1754
|
+
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1755
|
+
headers: ctx.headers,
|
|
1756
|
+
action,
|
|
1757
|
+
session: session.session,
|
|
1758
|
+
user: session.user
|
|
1759
|
+
})) throw new APIError("UNAUTHORIZED");
|
|
1760
|
+
}
|
|
1693
1761
|
//#endregion
|
|
1694
1762
|
//#region src/oauthClient/index.ts
|
|
1695
1763
|
const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/create-client", {
|
|
@@ -1875,6 +1943,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
1875
1943
|
}
|
|
1876
1944
|
}
|
|
1877
1945
|
}, async (ctx) => {
|
|
1946
|
+
await assertClientPrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
|
|
1878
1947
|
return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
|
|
1879
1948
|
});
|
|
1880
1949
|
const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client", {
|
|
@@ -2047,6 +2116,7 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
|
|
|
2047
2116
|
} }
|
|
2048
2117
|
} }
|
|
2049
2118
|
}, async (ctx) => {
|
|
2119
|
+
await assertClientPrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
|
|
2050
2120
|
return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
|
|
2051
2121
|
});
|
|
2052
2122
|
const getOAuthClient = (opts) => createAuthEndpoint("/oauth2/get-client", {
|
|
@@ -2459,6 +2529,7 @@ async function revokeAccessToken(ctx, opts, clientId, token) {
|
|
|
2459
2529
|
}
|
|
2460
2530
|
async function revokeEndpoint(ctx, opts) {
|
|
2461
2531
|
let { token, token_type_hint } = ctx.body;
|
|
2532
|
+
if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
|
|
2462
2533
|
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/revoke`));
|
|
2463
2534
|
if (!client_id) throw new APIError$1("UNAUTHORIZED", {
|
|
2464
2535
|
error_description: "missing required credentials",
|
|
@@ -2927,19 +2998,19 @@ const oauthProvider = (options) => {
|
|
|
2927
2998
|
if (opts.scopes && !opts.scopes.includes("openid")) throw new APIError("NOT_FOUND");
|
|
2928
2999
|
return oidcServerMetadata(ctx, opts);
|
|
2929
3000
|
}),
|
|
2930
|
-
oauth2Authorize:
|
|
3001
|
+
oauth2Authorize: createOAuthEndpoint("/oauth2/authorize", {
|
|
2931
3002
|
method: "GET",
|
|
2932
3003
|
query: z.object({
|
|
2933
|
-
response_type: z.enum(["code"]).optional(),
|
|
3004
|
+
response_type: z.string().pipe(z.enum(["code"])).optional(),
|
|
2934
3005
|
client_id: z.string(),
|
|
2935
3006
|
redirect_uri: SafeUrlSchema.optional(),
|
|
2936
3007
|
scope: z.string().optional(),
|
|
2937
3008
|
state: z.string().optional(),
|
|
2938
3009
|
request_uri: z.string().optional(),
|
|
2939
3010
|
code_challenge: z.string().optional(),
|
|
2940
|
-
code_challenge_method: z.enum(["S256"]).optional(),
|
|
3011
|
+
code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
|
|
2941
3012
|
nonce: z.string().optional(),
|
|
2942
|
-
prompt: z.enum([
|
|
3013
|
+
prompt: z.string().pipe(z.enum([
|
|
2943
3014
|
"none",
|
|
2944
3015
|
"consent",
|
|
2945
3016
|
"login",
|
|
@@ -2947,8 +3018,10 @@ const oauthProvider = (options) => {
|
|
|
2947
3018
|
"select_account",
|
|
2948
3019
|
"login consent",
|
|
2949
3020
|
"select_account consent"
|
|
2950
|
-
]).optional()
|
|
3021
|
+
])).optional()
|
|
2951
3022
|
}),
|
|
3023
|
+
redirectOnError: authorizeRedirectOnError(opts),
|
|
3024
|
+
errorCodesByField: { response_type: { invalid: "unsupported_response_type" } },
|
|
2952
3025
|
metadata: { openapi: {
|
|
2953
3026
|
description: "Authorize an OAuth2 request",
|
|
2954
3027
|
parameters: [
|
|
@@ -3107,14 +3180,14 @@ const oauthProvider = (options) => {
|
|
|
3107
3180
|
}, async (ctx) => {
|
|
3108
3181
|
return continueEndpoint(ctx, opts);
|
|
3109
3182
|
}),
|
|
3110
|
-
oauth2Token:
|
|
3183
|
+
oauth2Token: createOAuthEndpoint("/oauth2/token", {
|
|
3111
3184
|
method: "POST",
|
|
3112
3185
|
body: z.object({
|
|
3113
|
-
grant_type: z.enum([
|
|
3186
|
+
grant_type: z.string().pipe(z.enum([
|
|
3114
3187
|
"authorization_code",
|
|
3115
3188
|
"client_credentials",
|
|
3116
3189
|
"refresh_token"
|
|
3117
|
-
]),
|
|
3190
|
+
])),
|
|
3118
3191
|
client_id: z.string().optional(),
|
|
3119
3192
|
client_secret: z.string().optional(),
|
|
3120
3193
|
client_assertion: z.string().optional(),
|
|
@@ -3126,6 +3199,10 @@ const oauthProvider = (options) => {
|
|
|
3126
3199
|
resource: z.string().optional(),
|
|
3127
3200
|
scope: z.string().optional()
|
|
3128
3201
|
}),
|
|
3202
|
+
errorCodesByField: { grant_type: {
|
|
3203
|
+
missing: "invalid_request",
|
|
3204
|
+
invalid: "unsupported_grant_type"
|
|
3205
|
+
} },
|
|
3129
3206
|
metadata: {
|
|
3130
3207
|
allowedMediaTypes: ["application/x-www-form-urlencoded"],
|
|
3131
3208
|
openapi: {
|
|
@@ -3238,7 +3315,7 @@ const oauthProvider = (options) => {
|
|
|
3238
3315
|
}, async (ctx) => {
|
|
3239
3316
|
return tokenEndpoint(ctx, opts);
|
|
3240
3317
|
}),
|
|
3241
|
-
oauth2Introspect:
|
|
3318
|
+
oauth2Introspect: createOAuthEndpoint("/oauth2/introspect", {
|
|
3242
3319
|
method: "POST",
|
|
3243
3320
|
body: z.object({
|
|
3244
3321
|
client_id: z.string().optional(),
|
|
@@ -3246,7 +3323,7 @@ const oauthProvider = (options) => {
|
|
|
3246
3323
|
client_assertion: z.string().optional(),
|
|
3247
3324
|
client_assertion_type: z.string().optional(),
|
|
3248
3325
|
token: z.string(),
|
|
3249
|
-
token_type_hint: z.
|
|
3326
|
+
token_type_hint: z.string().optional()
|
|
3250
3327
|
}),
|
|
3251
3328
|
metadata: {
|
|
3252
3329
|
allowedMediaTypes: ["application/x-www-form-urlencoded"],
|
|
@@ -3271,8 +3348,7 @@ const oauthProvider = (options) => {
|
|
|
3271
3348
|
},
|
|
3272
3349
|
token_type_hint: {
|
|
3273
3350
|
type: "string",
|
|
3274
|
-
|
|
3275
|
-
description: "Hint about the type of the token submitted for introspection"
|
|
3351
|
+
description: "Hint about the token type. Recognized values: `access_token`, `refresh_token`."
|
|
3276
3352
|
},
|
|
3277
3353
|
resource: {
|
|
3278
3354
|
type: "string",
|
|
@@ -3358,7 +3434,7 @@ const oauthProvider = (options) => {
|
|
|
3358
3434
|
}, async (ctx) => {
|
|
3359
3435
|
return introspectEndpoint(ctx, opts);
|
|
3360
3436
|
}),
|
|
3361
|
-
oauth2Revoke:
|
|
3437
|
+
oauth2Revoke: createOAuthEndpoint("/oauth2/revoke", {
|
|
3362
3438
|
method: "POST",
|
|
3363
3439
|
body: z.object({
|
|
3364
3440
|
client_id: z.string().optional(),
|
|
@@ -3366,7 +3442,7 @@ const oauthProvider = (options) => {
|
|
|
3366
3442
|
client_assertion: z.string().optional(),
|
|
3367
3443
|
client_assertion_type: z.string().optional(),
|
|
3368
3444
|
token: z.string(),
|
|
3369
|
-
token_type_hint: z.
|
|
3445
|
+
token_type_hint: z.string().optional()
|
|
3370
3446
|
}),
|
|
3371
3447
|
metadata: {
|
|
3372
3448
|
allowedMediaTypes: ["application/x-www-form-urlencoded"],
|
|
@@ -3391,8 +3467,7 @@ const oauthProvider = (options) => {
|
|
|
3391
3467
|
},
|
|
3392
3468
|
token_type_hint: {
|
|
3393
3469
|
type: "string",
|
|
3394
|
-
|
|
3395
|
-
description: "Hint about the type of the token submitted for revocation"
|
|
3470
|
+
description: "Hint about the token type. Recognized values: `access_token`, `refresh_token`."
|
|
3396
3471
|
}
|
|
3397
3472
|
},
|
|
3398
3473
|
required: ["token"]
|
|
@@ -3513,7 +3588,7 @@ const oauthProvider = (options) => {
|
|
|
3513
3588
|
}, async (ctx) => {
|
|
3514
3589
|
return userInfoEndpoint(ctx, opts);
|
|
3515
3590
|
}),
|
|
3516
|
-
oauth2EndSession:
|
|
3591
|
+
oauth2EndSession: createOAuthEndpoint("/oauth2/end-session", {
|
|
3517
3592
|
method: "GET",
|
|
3518
3593
|
query: z.object({
|
|
3519
3594
|
id_token_hint: z.string(),
|
|
@@ -3544,7 +3619,7 @@ const oauthProvider = (options) => {
|
|
|
3544
3619
|
}, async (ctx) => {
|
|
3545
3620
|
return rpInitiatedLogoutEndpoint(ctx, opts);
|
|
3546
3621
|
}),
|
|
3547
|
-
registerOAuthClient:
|
|
3622
|
+
registerOAuthClient: createOAuthEndpoint("/oauth2/register", {
|
|
3548
3623
|
method: "POST",
|
|
3549
3624
|
body: z.object({
|
|
3550
3625
|
redirect_uris: z.array(SafeUrlSchema).min(1).min(1),
|
|
@@ -3581,6 +3656,12 @@ const oauthProvider = (options) => {
|
|
|
3581
3656
|
subject_type: z.enum(["public", "pairwise"]).optional(),
|
|
3582
3657
|
skip_consent: z.never({ error: "skip_consent cannot be set during dynamic client registration" }).optional()
|
|
3583
3658
|
}),
|
|
3659
|
+
errorCodesByField: {
|
|
3660
|
+
redirect_uris: "invalid_redirect_uri",
|
|
3661
|
+
post_logout_redirect_uris: "invalid_redirect_uri",
|
|
3662
|
+
software_statement: "invalid_software_statement"
|
|
3663
|
+
},
|
|
3664
|
+
defaultError: "invalid_client_metadata",
|
|
3584
3665
|
metadata: { openapi: {
|
|
3585
3666
|
description: "Register an OAuth2 application",
|
|
3586
3667
|
responses: { "200": {
|
|
@@ -3773,17 +3854,37 @@ const oauthProvider = (options) => {
|
|
|
3773
3854
|
//#endregion
|
|
3774
3855
|
//#region src/authorize.ts
|
|
3775
3856
|
/**
|
|
3776
|
-
* Formats an error url
|
|
3857
|
+
* Formats an error url. Per OIDC Core 1.0 §5 / RFC 6749 §4.2.2.1, errors on
|
|
3858
|
+
* implicit and hybrid flows are delivered in the URL fragment, not the query.
|
|
3859
|
+
* Callers on the code flow (default) omit `mode` and get query delivery.
|
|
3777
3860
|
*/
|
|
3778
|
-
function formatErrorURL(url, error, description, state, iss) {
|
|
3861
|
+
function formatErrorURL(url, error, description, state, iss, mode = "query") {
|
|
3779
3862
|
const searchParams = new URLSearchParams({
|
|
3780
3863
|
error,
|
|
3781
3864
|
error_description: description
|
|
3782
3865
|
});
|
|
3783
3866
|
state && searchParams.append("state", state);
|
|
3784
3867
|
iss && searchParams.append("iss", iss);
|
|
3868
|
+
if (mode === "fragment") return `${url}#${searchParams.toString()}`;
|
|
3785
3869
|
return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
|
|
3786
3870
|
}
|
|
3871
|
+
/**
|
|
3872
|
+
* Selects the response mode for an error redirect to the RP. OIDC Core 1.0 §5
|
|
3873
|
+
* defines defaults based on response_type: `code` → query, types containing
|
|
3874
|
+
* `token` / `id_token` → fragment. An explicit `response_mode` overrides.
|
|
3875
|
+
*
|
|
3876
|
+
* When `response_type` is duplicated (array) or absent, we can't trust the
|
|
3877
|
+
* caller's intent, so we default to query — the safer channel for
|
|
3878
|
+
* unrecognized shapes.
|
|
3879
|
+
*/
|
|
3880
|
+
function deriveResponseMode(raw) {
|
|
3881
|
+
const responseMode = typeof raw.response_mode === "string" ? raw.response_mode : void 0;
|
|
3882
|
+
if (responseMode === "fragment") return "fragment";
|
|
3883
|
+
if (responseMode === "query") return "query";
|
|
3884
|
+
const responseType = typeof raw.response_type === "string" ? raw.response_type : void 0;
|
|
3885
|
+
if (responseType && /\b(token|id_token)\b/.test(responseType)) return "fragment";
|
|
3886
|
+
return "query";
|
|
3887
|
+
}
|
|
3787
3888
|
const handleRedirect = (ctx, uri) => {
|
|
3788
3889
|
const fromFetch = isBrowserFetchRequest(ctx.request?.headers);
|
|
3789
3890
|
const acceptJson = ctx.headers?.get("accept")?.includes("application/json");
|
|
@@ -3807,8 +3908,7 @@ function redirectWithPromptNoneError(ctx, opts, query, error, description) {
|
|
|
3807
3908
|
function validateIssuerUrl(issuer) {
|
|
3808
3909
|
try {
|
|
3809
3910
|
const url = new URL(issuer);
|
|
3810
|
-
|
|
3811
|
-
if (url.protocol !== "https:" && !isLocalhost) url.protocol = "https:";
|
|
3911
|
+
if (url.protocol !== "https:" && !isLoopbackHost(url.host)) url.protocol = "https:";
|
|
3812
3912
|
url.search = "";
|
|
3813
3913
|
url.hash = "";
|
|
3814
3914
|
return url.toString().replace(/\/$/, "");
|
|
@@ -3836,6 +3936,64 @@ function getIssuer(ctx, opts) {
|
|
|
3836
3936
|
function getErrorURL(ctx, error, description) {
|
|
3837
3937
|
return formatErrorURL(ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`, error, description);
|
|
3838
3938
|
}
|
|
3939
|
+
/**
|
|
3940
|
+
* Finds the matching entry in a client's registered redirect_uris for a
|
|
3941
|
+
* requested redirect_uri. Honors RFC 8252 §7.3 loopback port variance for
|
|
3942
|
+
* the full 127.0.0.0/8 range and [::1], matching on scheme+host+path+query
|
|
3943
|
+
* and ignoring port. DNS names like "localhost" are excluded per §8.3.
|
|
3944
|
+
*/
|
|
3945
|
+
function findRegisteredRedirectUri(registered, requested) {
|
|
3946
|
+
if (!registered || !requested) return void 0;
|
|
3947
|
+
let req;
|
|
3948
|
+
try {
|
|
3949
|
+
req = new URL(requested);
|
|
3950
|
+
} catch {}
|
|
3951
|
+
return registered.find((url) => {
|
|
3952
|
+
if (url === requested) return true;
|
|
3953
|
+
if (!req) return false;
|
|
3954
|
+
try {
|
|
3955
|
+
const reg = new URL(url);
|
|
3956
|
+
return isLoopbackIP(reg.hostname) && reg.hostname === req.hostname && reg.pathname === req.pathname && reg.protocol === req.protocol && reg.search === req.search;
|
|
3957
|
+
} catch {
|
|
3958
|
+
return false;
|
|
3959
|
+
}
|
|
3960
|
+
});
|
|
3961
|
+
}
|
|
3962
|
+
/**
|
|
3963
|
+
* Loads the client, verifies it's enabled, and returns the requested
|
|
3964
|
+
* redirect_uri when it matches a registered entry. Returns null whenever the
|
|
3965
|
+
* RP cannot be safely reached, so callers can fall back to the server error
|
|
3966
|
+
* page (avoiding open-redirect risk on validation failures).
|
|
3967
|
+
*/
|
|
3968
|
+
async function resolveTrustedRedirectUri(ctx, opts, clientId, redirectUri) {
|
|
3969
|
+
if (!clientId || !redirectUri) return null;
|
|
3970
|
+
let client;
|
|
3971
|
+
try {
|
|
3972
|
+
client = await getClient(ctx, opts, clientId);
|
|
3973
|
+
} catch {
|
|
3974
|
+
return null;
|
|
3975
|
+
}
|
|
3976
|
+
if (!client || client.disabled) return null;
|
|
3977
|
+
return findRegisteredRedirectUri(client.redirectUris, redirectUri) ? redirectUri : null;
|
|
3978
|
+
}
|
|
3979
|
+
/**
|
|
3980
|
+
* `redirectOnError` callback for `/oauth2/authorize`. Per RFC 6749 §4.1.2.1,
|
|
3981
|
+
* authorize errors MUST be delivered to the client's `redirect_uri` with
|
|
3982
|
+
* `error`, `error_description`, `state`, and (RFC 9207) `iss`. The clause
|
|
3983
|
+
* carves out one case: a missing/invalid `redirect_uri` or `client_id` MUST
|
|
3984
|
+
* NOT redirect to the requested URI. We implement the carve-out via
|
|
3985
|
+
* `resolveTrustedRedirectUri`, falling back to the server error page.
|
|
3986
|
+
*
|
|
3987
|
+
* Channel (query vs fragment) follows OIDC Core §5 via `deriveResponseMode`.
|
|
3988
|
+
*/
|
|
3989
|
+
function authorizeRedirectOnError(opts) {
|
|
3990
|
+
return async ({ error, error_description, ctx }) => {
|
|
3991
|
+
const raw = ctx.query ?? {};
|
|
3992
|
+
const trusted = await resolveTrustedRedirectUri(ctx, opts, typeof raw.client_id === "string" ? raw.client_id : void 0, typeof raw.redirect_uri === "string" ? raw.redirect_uri : void 0);
|
|
3993
|
+
if (trusted) return handleRedirect(ctx, formatErrorURL(trusted, error, error_description, typeof raw.state === "string" ? raw.state : void 0, getIssuer(ctx, opts), deriveResponseMode(raw)));
|
|
3994
|
+
return handleRedirect(ctx, getErrorURL(ctx, error, error_description));
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3839
3997
|
async function authorizeEndpoint(ctx, opts, settings) {
|
|
3840
3998
|
if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError$1("NOT_FOUND");
|
|
3841
3999
|
if (!ctx.request) throw new APIError$1("UNAUTHORIZED", {
|
|
@@ -3866,15 +4024,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
3866
4024
|
const client = await getClient(ctx, opts, query.client_id);
|
|
3867
4025
|
if (!client) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "client_id is required"));
|
|
3868
4026
|
if (client.disabled) return handleRedirect(ctx, getErrorURL(ctx, "client_disabled", "client is disabled"));
|
|
3869
|
-
if (!client.redirectUris
|
|
3870
|
-
if (url === query.redirect_uri) return true;
|
|
3871
|
-
try {
|
|
3872
|
-
const registered = new URL(url);
|
|
3873
|
-
const requested = new URL(query.redirect_uri);
|
|
3874
|
-
if ((registered.hostname === "127.0.0.1" || registered.hostname === "[::1]") && registered.hostname === requested.hostname && registered.pathname === requested.pathname && registered.protocol === requested.protocol && registered.search === requested.search) return true;
|
|
3875
|
-
} catch {}
|
|
3876
|
-
return false;
|
|
3877
|
-
}) || !query.redirect_uri) return handleRedirect(ctx, getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
|
|
4027
|
+
if (!findRegisteredRedirectUri(client.redirectUris, query.redirect_uri) || !query.redirect_uri) return handleRedirect(ctx, getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
|
|
3878
4028
|
let requestedScopes = query.scope?.split(" ").filter((s) => s);
|
|
3879
4029
|
if (requestedScopes) {
|
|
3880
4030
|
const validScopes = new Set(client.scopes ?? opts.scopes);
|
|
@@ -5,6 +5,36 @@ import * as better_auth_plugins0 from "better-auth/plugins";
|
|
|
5
5
|
import * as jose from "jose";
|
|
6
6
|
import * as better_auth0 from "better-auth";
|
|
7
7
|
|
|
8
|
+
//#region src/oauth-endpoint.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* Canonical OAuth 2.0 / OpenID Connect error codes. The union is the single
|
|
11
|
+
* vocabulary for every error-emitting surface in this plugin: token, authorize,
|
|
12
|
+
* revoke, introspect, register, userinfo, logout, consent, and the redirect
|
|
13
|
+
* error channel. Entries are grouped by source RFC so the declaration doubles
|
|
14
|
+
* as a specification map.
|
|
15
|
+
*
|
|
16
|
+
* The trailing `(string & {})` keeps the type open for product-specific codes
|
|
17
|
+
* (e.g. `"invalid_verification"`, `"invalid_user"`) while preserving editor
|
|
18
|
+
* autocomplete for the listed standard codes. Prefer a standard code whenever
|
|
19
|
+
* one applies; fall back to a custom string only for states no RFC covers.
|
|
20
|
+
*/
|
|
21
|
+
type OAuthErrorCode = "invalid_request" | "invalid_client" | "invalid_grant" | "unauthorized_client" | "unsupported_grant_type" | "unsupported_response_type" | "invalid_scope" | "access_denied" | "server_error" | "temporarily_unavailable" | "invalid_token" | "unsupported_token_type" | "invalid_redirect_uri" | "invalid_client_metadata" | "invalid_software_statement" | "unapproved_software_statement" | "invalid_target" | "invalid_request_object" | "login_required" | "consent_required" | "interaction_required" | "account_selection_required" | "invalid_request_uri" | "request_not_supported" | "request_uri_not_supported" | "registration_not_supported" | (string & {});
|
|
22
|
+
type OAuthFieldErrorCodeMap = {
|
|
23
|
+
missing?: OAuthErrorCode;
|
|
24
|
+
invalid?: OAuthErrorCode;
|
|
25
|
+
};
|
|
26
|
+
type OAuthFieldErrorCode = OAuthErrorCode | OAuthFieldErrorCodeMap;
|
|
27
|
+
interface OAuthEndpointErrorResult {
|
|
28
|
+
error: OAuthErrorCode;
|
|
29
|
+
error_description: string;
|
|
30
|
+
}
|
|
31
|
+
interface OAuthEndpointRedirectContext<Ctx = unknown> {
|
|
32
|
+
error: OAuthErrorCode;
|
|
33
|
+
error_description: string;
|
|
34
|
+
ctx: Ctx;
|
|
35
|
+
}
|
|
36
|
+
type OAuthRedirectOnError<Ctx = any> = (result: OAuthEndpointRedirectContext<Ctx>) => unknown;
|
|
37
|
+
//#endregion
|
|
8
38
|
//#region src/oauth.d.ts
|
|
9
39
|
declare module "@better-auth/core" {
|
|
10
40
|
interface BetterAuthPluginRegistry<AuthOptions, Options> {
|
|
@@ -160,20 +190,20 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
160
190
|
oauth2Authorize: better_call0.StrictEndpoint<"/oauth2/authorize", {
|
|
161
191
|
method: "GET";
|
|
162
192
|
query: z.ZodObject<{
|
|
163
|
-
response_type: z.ZodOptional<z.ZodEnum<{
|
|
193
|
+
response_type: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodEnum<{
|
|
164
194
|
code: "code";
|
|
165
|
-
}
|
|
195
|
+
}>>>;
|
|
166
196
|
client_id: z.ZodString;
|
|
167
197
|
redirect_uri: z.ZodOptional<z.ZodURL>;
|
|
168
198
|
scope: z.ZodOptional<z.ZodString>;
|
|
169
199
|
state: z.ZodOptional<z.ZodString>;
|
|
170
200
|
request_uri: z.ZodOptional<z.ZodString>;
|
|
171
201
|
code_challenge: z.ZodOptional<z.ZodString>;
|
|
172
|
-
code_challenge_method: z.ZodOptional<z.ZodEnum<{
|
|
202
|
+
code_challenge_method: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodEnum<{
|
|
173
203
|
S256: "S256";
|
|
174
|
-
}
|
|
204
|
+
}>>>;
|
|
175
205
|
nonce: z.ZodOptional<z.ZodString>;
|
|
176
|
-
prompt: z.ZodOptional<z.ZodEnum<{
|
|
206
|
+
prompt: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodEnum<{
|
|
177
207
|
none: "none";
|
|
178
208
|
consent: "consent";
|
|
179
209
|
login: "login";
|
|
@@ -181,8 +211,14 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
181
211
|
select_account: "select_account";
|
|
182
212
|
"login consent": "login consent";
|
|
183
213
|
"select_account consent": "select_account consent";
|
|
184
|
-
}
|
|
214
|
+
}>>>;
|
|
185
215
|
}, z.core.$strip>;
|
|
216
|
+
redirectOnError: OAuthRedirectOnError<better_auth0.GenericEndpointContext>;
|
|
217
|
+
errorCodesByField: {
|
|
218
|
+
response_type: {
|
|
219
|
+
invalid: "unsupported_response_type";
|
|
220
|
+
};
|
|
221
|
+
};
|
|
186
222
|
metadata: {
|
|
187
223
|
openapi: {
|
|
188
224
|
description: string;
|
|
@@ -378,11 +414,11 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
378
414
|
oauth2Token: better_call0.StrictEndpoint<"/oauth2/token", {
|
|
379
415
|
method: "POST";
|
|
380
416
|
body: z.ZodObject<{
|
|
381
|
-
grant_type: z.ZodEnum<{
|
|
417
|
+
grant_type: z.ZodPipe<z.ZodString, z.ZodEnum<{
|
|
382
418
|
authorization_code: "authorization_code";
|
|
383
419
|
client_credentials: "client_credentials";
|
|
384
420
|
refresh_token: "refresh_token";
|
|
385
|
-
}
|
|
421
|
+
}>>;
|
|
386
422
|
client_id: z.ZodOptional<z.ZodString>;
|
|
387
423
|
client_secret: z.ZodOptional<z.ZodString>;
|
|
388
424
|
client_assertion: z.ZodOptional<z.ZodString>;
|
|
@@ -394,6 +430,12 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
394
430
|
resource: z.ZodOptional<z.ZodString>;
|
|
395
431
|
scope: z.ZodOptional<z.ZodString>;
|
|
396
432
|
}, z.core.$strip>;
|
|
433
|
+
errorCodesByField: {
|
|
434
|
+
grant_type: {
|
|
435
|
+
missing: "invalid_request";
|
|
436
|
+
invalid: "unsupported_grant_type";
|
|
437
|
+
};
|
|
438
|
+
};
|
|
397
439
|
metadata: {
|
|
398
440
|
allowedMediaTypes: string[];
|
|
399
441
|
openapi: {
|
|
@@ -530,10 +572,7 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
530
572
|
client_assertion: z.ZodOptional<z.ZodString>;
|
|
531
573
|
client_assertion_type: z.ZodOptional<z.ZodString>;
|
|
532
574
|
token: z.ZodString;
|
|
533
|
-
token_type_hint: z.ZodOptional<z.
|
|
534
|
-
refresh_token: "refresh_token";
|
|
535
|
-
access_token: "access_token";
|
|
536
|
-
}>>;
|
|
575
|
+
token_type_hint: z.ZodOptional<z.ZodString>;
|
|
537
576
|
}, z.core.$strip>;
|
|
538
577
|
metadata: {
|
|
539
578
|
allowedMediaTypes: string[];
|
|
@@ -560,7 +599,6 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
560
599
|
};
|
|
561
600
|
token_type_hint: {
|
|
562
601
|
type: string;
|
|
563
|
-
enum: string[];
|
|
564
602
|
description: string;
|
|
565
603
|
};
|
|
566
604
|
resource: {
|
|
@@ -669,10 +707,7 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
669
707
|
client_assertion: z.ZodOptional<z.ZodString>;
|
|
670
708
|
client_assertion_type: z.ZodOptional<z.ZodString>;
|
|
671
709
|
token: z.ZodString;
|
|
672
|
-
token_type_hint: z.ZodOptional<z.
|
|
673
|
-
refresh_token: "refresh_token";
|
|
674
|
-
access_token: "access_token";
|
|
675
|
-
}>>;
|
|
710
|
+
token_type_hint: z.ZodOptional<z.ZodString>;
|
|
676
711
|
}, z.core.$strip>;
|
|
677
712
|
metadata: {
|
|
678
713
|
allowedMediaTypes: string[];
|
|
@@ -699,7 +734,6 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
699
734
|
};
|
|
700
735
|
token_type_hint: {
|
|
701
736
|
type: string;
|
|
702
|
-
enum: string[];
|
|
703
737
|
description: string;
|
|
704
738
|
};
|
|
705
739
|
};
|
|
@@ -951,6 +985,12 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
951
985
|
}>>;
|
|
952
986
|
skip_consent: z.ZodOptional<z.ZodNever>;
|
|
953
987
|
}, z.core.$strip>;
|
|
988
|
+
errorCodesByField: {
|
|
989
|
+
redirect_uris: "invalid_redirect_uri";
|
|
990
|
+
post_logout_redirect_uris: "invalid_redirect_uri";
|
|
991
|
+
software_statement: "invalid_software_statement";
|
|
992
|
+
};
|
|
993
|
+
defaultError: "invalid_client_metadata";
|
|
954
994
|
metadata: {
|
|
955
995
|
openapi: {
|
|
956
996
|
description: string;
|
|
@@ -2196,4 +2236,4 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
|
|
|
2196
2236
|
})[];
|
|
2197
2237
|
};
|
|
2198
2238
|
//#endregion
|
|
2199
|
-
export { oauthProvider as n, getOAuthProviderState as t };
|
|
2239
|
+
export { OAuthErrorCode as a, OAuthRedirectOnError as c, OAuthEndpointRedirectContext as i, oauthProvider as n, OAuthFieldErrorCode as o, OAuthEndpointErrorResult as r, OAuthFieldErrorCodeMap as s, getOAuthProviderState as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/oauth-provider",
|
|
3
|
-
"version": "1.7.0-beta.
|
|
3
|
+
"version": "1.7.0-beta.2",
|
|
4
4
|
"description": "An oauth provider plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,15 +64,15 @@
|
|
|
64
64
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
65
65
|
"listhen": "^1.9.0",
|
|
66
66
|
"tsdown": "0.21.1",
|
|
67
|
-
"@better-auth/core": "1.7.0-beta.
|
|
68
|
-
"better-auth": "1.7.0-beta.
|
|
67
|
+
"@better-auth/core": "1.7.0-beta.2",
|
|
68
|
+
"better-auth": "1.7.0-beta.2"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"@better-auth/utils": "0.4.0",
|
|
72
72
|
"@better-fetch/fetch": "1.1.21",
|
|
73
73
|
"better-call": "1.3.5",
|
|
74
|
-
"@better-auth/core": "^1.7.0-beta.
|
|
75
|
-
"better-auth": "^1.7.0-beta.
|
|
74
|
+
"@better-auth/core": "^1.7.0-beta.2",
|
|
75
|
+
"better-auth": "^1.7.0-beta.2"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"build": "tsdown",
|