better-auth 1.7.19 → 1.7.21
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/api/index.d.mts +2 -1
- package/dist/api/index.mjs +2 -1
- package/dist/api/routes/sign-up.mjs +21 -10
- package/dist/db/get-migration.mjs +9 -11
- package/dist/db/internal-adapter.mjs +4 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/device-authorization/routes.mjs +15 -0
- package/dist/plugins/generic-oauth/index.d.mts +1 -4
- package/dist/plugins/generic-oauth/index.mjs +38 -23
- package/dist/plugins/generic-oauth/types.d.mts +5 -2
- package/dist/plugins/siwe/index.mjs +1 -1
- package/dist/state.d.mts +2 -5
- package/dist/state.mjs +1 -4
- package/dist/utils/hide-metadata.d.mts +2 -5
- package/dist/utils/hide-metadata.mjs +1 -4
- package/package.json +8 -8
package/dist/api/index.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OverrideMerge, Prettify as Prettify$1, UnionToIntersection } from "../types/helper.mjs";
|
|
2
2
|
import { AdditionalSessionFieldsInput, AdditionalUserFieldsInput } from "../types/models.mjs";
|
|
3
3
|
import "../types/index.mjs";
|
|
4
|
+
import { generateGenericState, parseGenericState } from "../state.mjs";
|
|
4
5
|
import "../index.mjs";
|
|
5
6
|
import { isAPIError } from "../utils/is-api-error.mjs";
|
|
6
7
|
import { DispatchContext, dispatchAuthEndpoint } from "./dispatch.mjs";
|
|
@@ -4049,4 +4050,4 @@ export declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext
|
|
|
4049
4050
|
} extends (infer T_2) ? { [K in keyof T_2 as K extends keyof T_1 ? never : K]: T_2[K]; } : never) & T_1> : never : never : never;
|
|
4050
4051
|
};
|
|
4051
4052
|
//#endregion
|
|
4052
|
-
export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, markSessionOutsideStore, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
|
4053
|
+
export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, generateGenericState, getAccessToken, getAuthoritativeSessionFromCtx, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, markSessionOutsideStore, ok, optionsMiddleware, originCheck, originCheckMiddleware, parseGenericState, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
package/dist/api/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { onRequestRateLimit } from "./rate-limiter/index.mjs";
|
|
|
5
5
|
import { getShouldSkipSessionRefresh, setShouldSkipSessionRefresh } from "./state/should-session-refresh.mjs";
|
|
6
6
|
import { freshSessionMiddleware, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
|
|
7
7
|
import { addOAuthServerContext, getOAuthState } from "./state/oauth.mjs";
|
|
8
|
+
import { generateGenericState, parseGenericState } from "../state.mjs";
|
|
8
9
|
import { accountInfo, getAccessToken, linkSocialAccount, listUserAccounts, refreshToken, unlinkAccount } from "./routes/account.mjs";
|
|
9
10
|
import { callbackOAuth } from "./routes/callback.mjs";
|
|
10
11
|
import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEmailFn, verifyEmail } from "./routes/email-verification.mjs";
|
|
@@ -223,4 +224,4 @@ const router = (ctx, options) => {
|
|
|
223
224
|
});
|
|
224
225
|
};
|
|
225
226
|
//#endregion
|
|
226
|
-
export { APIError, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getEndpoints, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, markSessionOutsideStore, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
|
227
|
+
export { APIError, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, generateGenericState, getAccessToken, getAuthoritativeSessionFromCtx, getEndpoints, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, markSessionOutsideStore, ok, optionsMiddleware, originCheck, originCheckMiddleware, parseGenericState, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
|
@@ -5,7 +5,7 @@ import { setSessionCookie } from "../../cookies/index.mjs";
|
|
|
5
5
|
import { safeCloneRequest } from "../../utils/request.mjs";
|
|
6
6
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
7
7
|
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
8
|
-
import { runWithTransaction } from "@better-auth/core/context";
|
|
8
|
+
import { queueAfterTransactionHook, runWithTransaction } from "@better-auth/core/context";
|
|
9
9
|
import { isDevelopment } from "@better-auth/core/env";
|
|
10
10
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
11
11
|
import { generateId } from "@better-auth/core/utils/id";
|
|
@@ -198,7 +198,13 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
198
198
|
* between existing and non-existing emails.
|
|
199
199
|
*/
|
|
200
200
|
await ctx.context.password.hash(password);
|
|
201
|
-
|
|
201
|
+
const onExistingUserSignUp = ctx.context.options.emailAndPassword?.onExistingUserSignUp;
|
|
202
|
+
if (onExistingUserSignUp) {
|
|
203
|
+
const request = safeCloneRequest(ctx.request);
|
|
204
|
+
await queueAfterTransactionHook(async () => {
|
|
205
|
+
await ctx.context.runInBackgroundOrAwait(onExistingUserSignUp({ user: dbUser.user }, request));
|
|
206
|
+
}, { onError: (error) => ctx.context.logger.error("Failed to run onExistingUserSignUp after sign-up", error) });
|
|
207
|
+
}
|
|
202
208
|
return buildGenericDuplicateResponse();
|
|
203
209
|
}
|
|
204
210
|
throw APIError.from("UNPROCESSABLE_ENTITY", BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
|
|
@@ -238,15 +244,20 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
238
244
|
accountId: createdUser.id,
|
|
239
245
|
password: hash
|
|
240
246
|
});
|
|
241
|
-
|
|
242
|
-
|
|
247
|
+
const shouldSendVerificationEmail = ctx.context.options.emailVerification?.sendOnSignUp ?? ctx.context.options.emailAndPassword.requireEmailVerification;
|
|
248
|
+
const sendVerificationEmail = ctx.context.options.emailVerification?.sendVerificationEmail;
|
|
249
|
+
if (shouldSendVerificationEmail && sendVerificationEmail) {
|
|
243
250
|
const callbackURL = body.callbackURL ? encodeURIComponent(body.callbackURL) : encodeURIComponent("/");
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
url
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
const request = safeCloneRequest(ctx.request);
|
|
252
|
+
await queueAfterTransactionHook(async () => {
|
|
253
|
+
const token = await createEmailVerificationToken(ctx.context.secret, createdUser.email, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: createdUser.id });
|
|
254
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
|
|
255
|
+
await ctx.context.runInBackgroundOrAwait(sendVerificationEmail({
|
|
256
|
+
user: createdUser,
|
|
257
|
+
url,
|
|
258
|
+
token
|
|
259
|
+
}, request));
|
|
260
|
+
}, { onError: (error) => ctx.context.logger.error("Failed to send the sign-up verification email", error) });
|
|
250
261
|
}
|
|
251
262
|
if (shouldSkipAutoSignIn) return ctx.json({
|
|
252
263
|
token: null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildSchema } from "./get-schema.mjs";
|
|
2
2
|
import { createLogger } from "@better-auth/core/env";
|
|
3
3
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
4
|
-
import { createKyselyAdapter, getMssqlSchema, getPostgresSchema, toIntrospectedTables, toPhysicalSchema } from "@better-auth/kysely-adapter";
|
|
4
|
+
import { createKyselyAdapter, getMssqlSchema, getPostgresSchema, introspectPostgresTables, toIntrospectedTables, toPhysicalSchema } from "@better-auth/kysely-adapter";
|
|
5
5
|
import { initGetFieldName, initGetModelName } from "@better-auth/core/db/adapter";
|
|
6
6
|
import { diffSchema, formatSchemaFinding, getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, invalidateSchemaChecks } from "@better-auth/core/db/internal";
|
|
7
7
|
import { sql } from "kysely";
|
|
@@ -354,9 +354,8 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
354
354
|
logger.error("Only kysely adapter is supported for migrations. You can use `generate` command to generate the schema, if you're using a different adapter.");
|
|
355
355
|
process.exit(1);
|
|
356
356
|
}
|
|
357
|
-
const allTableMetadata = await db.introspection.getTables();
|
|
358
357
|
let target;
|
|
359
|
-
let tableMetadata
|
|
358
|
+
let tableMetadata;
|
|
360
359
|
switch (dbType) {
|
|
361
360
|
case "postgres": {
|
|
362
361
|
const schema = schemaName ?? await getPostgresSchema(db);
|
|
@@ -373,11 +372,7 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
373
372
|
} catch (error) {
|
|
374
373
|
logger.debug(`Could not verify schema existence: ${error instanceof Error ? error.message : String(error)}`);
|
|
375
374
|
}
|
|
376
|
-
|
|
377
|
-
* Kysely 0.28 does not expose `isForeign`, while 0.29 adds foreign table metadata.
|
|
378
|
-
* @see https://github.com/kysely-org/kysely/pull/1494
|
|
379
|
-
*/
|
|
380
|
-
tableMetadata = allTableMetadata.filter((table) => table.schema === schema && !table.isView && !("isForeign" in table && table.isForeign));
|
|
375
|
+
tableMetadata = (await introspectPostgresTables(db, [schema])).filter((table) => !table.isView && !table.isForeign);
|
|
381
376
|
logger.debug(`Found ${tableMetadata.length} table(s) in schema '${schema}': ${tableMetadata.map((table) => table.name).join(", ") || "(none)"}`);
|
|
382
377
|
break;
|
|
383
378
|
}
|
|
@@ -388,15 +383,18 @@ async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
|
388
383
|
schema
|
|
389
384
|
};
|
|
390
385
|
logger.debug(`SQL Server migration: Using schema '${schema}' (from the current user's default schema)`);
|
|
391
|
-
tableMetadata =
|
|
386
|
+
tableMetadata = (await db.introspection.getTables()).filter((table) => table.schema === schema);
|
|
392
387
|
break;
|
|
393
388
|
}
|
|
394
389
|
case "mysql":
|
|
395
390
|
target = { type: "mysql" };
|
|
391
|
+
tableMetadata = await db.introspection.getTables();
|
|
396
392
|
break;
|
|
397
|
-
case "sqlite":
|
|
393
|
+
case "sqlite":
|
|
394
|
+
target = { type: "sqlite" };
|
|
395
|
+
tableMetadata = await db.introspection.getTables();
|
|
398
396
|
}
|
|
399
|
-
const databaseIndexMap = await getDatabaseIndexMap(db, target,
|
|
397
|
+
const databaseIndexMap = await getDatabaseIndexMap(db, target, tableMetadata.map((table) => table.name), introspectIndexes);
|
|
400
398
|
const databaseColumnBounds = await getDatabaseColumnBounds(db, target);
|
|
401
399
|
const schemaProblems = diffSchema(toPhysicalSchema(db, betterAuthSchema), toIntrospectedTables(tableMetadata)).filter((finding) => finding.kind === "unexpected-required-column").map((finding) => formatSchemaFinding(finding, "database"));
|
|
402
400
|
const toBeCreated = [];
|
|
@@ -930,7 +930,10 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
930
930
|
* is best-effort there (it is intended for development and tests).
|
|
931
931
|
*/
|
|
932
932
|
reserveVerificationValue: async (data) => {
|
|
933
|
-
const reservationId = encodeDeterministicId(new Uint8Array(await createHash("SHA-256").digest(new TextEncoder().encode("reserve:" + data.identifier))), options
|
|
933
|
+
const reservationId = encodeDeterministicId(new Uint8Array(await createHash("SHA-256").digest(new TextEncoder().encode("reserve:" + data.identifier))), options, {
|
|
934
|
+
adapter,
|
|
935
|
+
model: "verification"
|
|
936
|
+
});
|
|
934
937
|
const storageOption = getStorageOption(data.identifier, options.verification?.storeIdentifier);
|
|
935
938
|
const storedIdentifier = await processIdentifier(data.identifier, storageOption);
|
|
936
939
|
if (secondaryStorage && !options.verification?.storeInDatabase) throw new BetterAuthError("reserveVerificationValue requires database-backed verification storage. Set verification.storeInDatabase to true for flows that reserve verification values.");
|
package/dist/package.mjs
CHANGED
|
@@ -550,6 +550,21 @@ const deviceVerify = (grant) => createAuthEndpoint("/device", {
|
|
|
550
550
|
increment: {},
|
|
551
551
|
set: { userId: session.user.id }
|
|
552
552
|
})) deviceCodeRecord.userId = session.user.id;
|
|
553
|
+
else {
|
|
554
|
+
const current = await ctx.context.adapter.findOne({
|
|
555
|
+
model: "deviceCode",
|
|
556
|
+
where: [{
|
|
557
|
+
field: "id",
|
|
558
|
+
value: deviceCodeRecord.id
|
|
559
|
+
}]
|
|
560
|
+
});
|
|
561
|
+
if (!current) throw new APIError("BAD_REQUEST", {
|
|
562
|
+
error: "invalid_request",
|
|
563
|
+
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE.message
|
|
564
|
+
});
|
|
565
|
+
deviceCodeRecord.userId = current.userId;
|
|
566
|
+
deviceCodeRecord.status = current.status;
|
|
567
|
+
}
|
|
553
568
|
}
|
|
554
569
|
const canReviewRequest = session?.user.id !== void 0 && deviceCodeRecord.userId === session.user.id;
|
|
555
570
|
const grantContext = canReviewRequest ? grant?.getVerificationContext(deviceCodeRecord) : void 0;
|
|
@@ -25,10 +25,7 @@ declare module "@better-auth/core" {
|
|
|
25
25
|
* Extracts common fields from GenericOAuthConfig for provider helpers.
|
|
26
26
|
*/
|
|
27
27
|
export type BaseOAuthProviderOptions = Pick<GenericOAuthConfig, "clientId" | "clientSecret" | "tokenEndpointAuth" | "scopes" | "redirectURI" | "endSessionEndpoint" | "postLogoutRedirectURI" | "disableProviderLogout" | "pkce" | "disableImplicitSignUp" | "disableSignUp" | "overrideUserInfo">;
|
|
28
|
-
type PendingDiscovery = Map<string,
|
|
29
|
-
config: GenericOAuthConfig;
|
|
30
|
-
attempt?: Promise<void> | undefined;
|
|
31
|
-
}>;
|
|
28
|
+
type PendingDiscovery = Map<string, GenericOAuthConfig>;
|
|
32
29
|
/**
|
|
33
30
|
* A generic OAuth plugin that registers any OAuth/OIDC provider
|
|
34
31
|
* as a first-class social provider.
|
|
@@ -25,6 +25,7 @@ function isClientSecretTokenEndpointAuth(tokenEndpointAuth) {
|
|
|
25
25
|
}
|
|
26
26
|
/** A request waits on a discovery retry, so bound it (the SSO plugin uses 10s too). */
|
|
27
27
|
const DISCOVERY_RETRY_TIMEOUT_MS = 1e4;
|
|
28
|
+
const MAX_DISCOVERY_REDIRECTS = 5;
|
|
28
29
|
/**
|
|
29
30
|
* Providers skipped because startup discovery failed. Init stores them on each auth
|
|
30
31
|
* instance's context, so one plugin object can serve several instances. It is a context
|
|
@@ -44,18 +45,35 @@ const readIdTokenClaims = (idToken) => {
|
|
|
44
45
|
}
|
|
45
46
|
};
|
|
46
47
|
async function fetchDiscovery(url, headers, timeout) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
const signal = timeout ? AbortSignal.timeout(timeout) : void 0;
|
|
49
|
+
let target = url;
|
|
50
|
+
for (let hop = 0;; hop++) {
|
|
51
|
+
const redirect = {};
|
|
52
|
+
const result = await betterFetch(target, {
|
|
53
|
+
method: "GET",
|
|
54
|
+
headers,
|
|
55
|
+
redirect: headers ? "manual" : void 0,
|
|
56
|
+
signal,
|
|
57
|
+
onError: ({ response }) => {
|
|
58
|
+
if (response.status >= 300 && response.status < 400) redirect.location = response.headers.get("location");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if (headers && redirect.location) {
|
|
62
|
+
const next = new URL(redirect.location, target);
|
|
63
|
+
if (next.origin !== new URL(url).origin) throw new Error(`discovery redirected to ${next.origin}; with discoveryHeaders set, only same-origin redirects are followed. Set discoveryUrl to the final URL.`);
|
|
64
|
+
if (hop >= MAX_DISCOVERY_REDIRECTS) throw new Error(`discovery redirected more than ${MAX_DISCOVERY_REDIRECTS} times. Set discoveryUrl to the final URL.`);
|
|
65
|
+
target = next.href;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (headers && result.error?.status === 0) throw new Error("discovery redirected; with discoveryHeaders set, set discoveryUrl to the final URL.");
|
|
69
|
+
if (result.error || !result.data) return null;
|
|
70
|
+
if (result.data.issuer) try {
|
|
71
|
+
new URL(result.data.issuer);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return result.data;
|
|
57
76
|
}
|
|
58
|
-
return result.data;
|
|
59
77
|
}
|
|
60
78
|
async function fetchUserInfo(tokens, userInfoUrl, idTokenClaims) {
|
|
61
79
|
if (tokens.idToken) {
|
|
@@ -128,8 +146,9 @@ const genericOAuth = (options) => {
|
|
|
128
146
|
ctx.logger.error(`Provider "${c.providerId}": invalid jwks_uri "${discovered.jwks_uri}" in discovery document. Provider skipped.`);
|
|
129
147
|
continue;
|
|
130
148
|
}
|
|
149
|
+
const jwksHeaders = jwksUrl.origin === new URL(c.discoveryUrl).origin ? c.discoveryHeaders : void 0;
|
|
131
150
|
idTokenConfig = {
|
|
132
|
-
jwks: createRemoteJWKSet(jwksUrl),
|
|
151
|
+
jwks: createRemoteJWKSet(jwksUrl, jwksHeaders && { headers: jwksHeaders }),
|
|
133
152
|
issuer: discovered.issuer,
|
|
134
153
|
audience: c.clientId,
|
|
135
154
|
algorithms: isOidc ? signingAlgs : void 0
|
|
@@ -287,7 +306,8 @@ const genericOAuth = (options) => {
|
|
|
287
306
|
authentication: c.authentication,
|
|
288
307
|
tokenEndpointAuth,
|
|
289
308
|
tokenEndpoint: tokenUrl,
|
|
290
|
-
extraParams: resolvedRefreshParams
|
|
309
|
+
extraParams: resolvedRefreshParams,
|
|
310
|
+
headers: c.authorizationHeaders
|
|
291
311
|
});
|
|
292
312
|
return applyDefaultAccessTokenExpiry(tokens, c.accessTokenExpiresIn);
|
|
293
313
|
},
|
|
@@ -303,18 +323,13 @@ const genericOAuth = (options) => {
|
|
|
303
323
|
}
|
|
304
324
|
return genericProviders;
|
|
305
325
|
};
|
|
306
|
-
const retryDiscovery = (ctx, providerId) => {
|
|
326
|
+
const retryDiscovery = async (ctx, providerId) => {
|
|
307
327
|
const pendingDiscovery = getPendingDiscovery(ctx);
|
|
308
328
|
const pending = pendingDiscovery?.get(providerId);
|
|
309
329
|
if (!pending) return;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
ctx.socialProviders.unshift(provider);
|
|
314
|
-
}).finally(() => {
|
|
315
|
-
pending.attempt = void 0;
|
|
316
|
-
});
|
|
317
|
-
return pending.attempt;
|
|
330
|
+
const [provider] = await buildProviders([pending], ctx, DISCOVERY_RETRY_TIMEOUT_MS);
|
|
331
|
+
if (!provider || !pendingDiscovery?.delete(providerId)) return;
|
|
332
|
+
ctx.socialProviders.unshift(provider);
|
|
318
333
|
};
|
|
319
334
|
const requestedProviderId = (context) => {
|
|
320
335
|
const id = context.body?.provider ?? context.body?.providerId ?? context.query?.provider ?? context.params?.id;
|
|
@@ -326,7 +341,7 @@ const genericOAuth = (options) => {
|
|
|
326
341
|
init: async (ctx) => {
|
|
327
342
|
const genericProviders = await buildProviders(options.config, ctx);
|
|
328
343
|
const built = new Set(genericProviders.map((p) => p.id));
|
|
329
|
-
const genericOAuthPendingDiscovery = new Map(options.config.filter((c) => !built.has(c.providerId)).map((c) => [c.providerId,
|
|
344
|
+
const genericOAuthPendingDiscovery = new Map(options.config.filter((c) => !built.has(c.providerId)).map((c) => [c.providerId, c]));
|
|
330
345
|
const existingIds = new Set(ctx.socialProviders.map((p) => p.id));
|
|
331
346
|
for (const gp of genericProviders) if (existingIds.has(gp.id)) ctx.logger.warn(`Generic OAuth provider "${gp.id}" shadows a built-in social provider with the same ID`);
|
|
332
347
|
return { context: {
|
|
@@ -211,12 +211,15 @@ export interface GenericOAuthConfig<ID extends string = string> {
|
|
|
211
211
|
*/
|
|
212
212
|
authentication?: ("basic" | "post") | undefined;
|
|
213
213
|
/**
|
|
214
|
-
* Custom headers to include in the discovery request
|
|
214
|
+
* Custom headers to include in the discovery request, and in the JWKS request when
|
|
215
|
+
* `jwks_uri` has the same origin as `discoveryUrl`. When set, discovery follows
|
|
216
|
+
* redirects only within that origin and fails on a redirect to another origin.
|
|
215
217
|
* Useful for providers like Epic that require specific headers (e.g., Epic-Client-ID).
|
|
216
218
|
*/
|
|
217
219
|
discoveryHeaders?: Record<string, string> | undefined;
|
|
218
220
|
/**
|
|
219
|
-
* Custom headers to include in the authorization
|
|
221
|
+
* Custom headers to include in token endpoint requests: the authorization
|
|
222
|
+
* code exchange and token refresh.
|
|
220
223
|
* Useful for providers like Qonto that require specific headers (e.g., X-Qonto-Staging-Token for local development).
|
|
221
224
|
*/
|
|
222
225
|
authorizationHeaders?: Record<string, string> | undefined;
|
|
@@ -274,9 +274,9 @@ const siwe = (options) => {
|
|
|
274
274
|
});
|
|
275
275
|
} catch (error) {
|
|
276
276
|
if (isAPIError(error)) throw error;
|
|
277
|
+
ctx.context.logger.error("Failed to verify SIWE message", error);
|
|
277
278
|
throw APIError.fromStatus("UNAUTHORIZED", {
|
|
278
279
|
message: "Something went wrong. Please try again later.",
|
|
279
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
280
280
|
status: 401
|
|
281
281
|
});
|
|
282
282
|
}
|
package/dist/state.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GenericEndpointContext } from "@better-auth/core";
|
|
2
2
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { getAuthStateVerificationIdentifier } from "@better-auth/core/utils/state";
|
|
4
5
|
//#region src/state.d.ts
|
|
5
6
|
declare const stateDataSchema: z.ZodObject<{
|
|
6
7
|
callbackURL: z.ZodString;
|
|
@@ -19,10 +20,6 @@ declare const stateDataSchema: z.ZodObject<{
|
|
|
19
20
|
serverContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
20
21
|
}, z.core.$loose>;
|
|
21
22
|
export type StateData = z.infer<typeof stateDataSchema>;
|
|
22
|
-
/**
|
|
23
|
-
* Verification key for database-backed OAuth and SAML relay state.
|
|
24
|
-
*/
|
|
25
|
-
export declare const getAuthStateVerificationIdentifier: (state: string) => string;
|
|
26
23
|
export declare function generateGenericState(c: GenericEndpointContext, stateData: StateData, settings?: {
|
|
27
24
|
cookieName: string;
|
|
28
25
|
}): Promise<{
|
|
@@ -50,4 +47,4 @@ export declare function parseGenericState(c: GenericEndpointContext, state: stri
|
|
|
50
47
|
serverContext?: Record<string, unknown> | undefined;
|
|
51
48
|
}>;
|
|
52
49
|
//#endregion
|
|
53
|
-
export {};
|
|
50
|
+
export { getAuthStateVerificationIdentifier };
|
package/dist/state.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { expireCookie } from "./cookies/index.mjs";
|
|
|
4
4
|
import { derivePurposeKey } from "./crypto/purpose.mjs";
|
|
5
5
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
6
6
|
import * as z from "zod";
|
|
7
|
+
import { getAuthStateVerificationIdentifier } from "@better-auth/core/utils/state";
|
|
7
8
|
//#region src/state.ts
|
|
8
9
|
const stateDataSchema = z.looseObject({
|
|
9
10
|
callbackURL: z.string(),
|
|
@@ -34,10 +35,6 @@ const stateDataSchema = z.looseObject({
|
|
|
34
35
|
*/
|
|
35
36
|
serverContext: z.record(z.string(), z.unknown()).optional()
|
|
36
37
|
});
|
|
37
|
-
/**
|
|
38
|
-
* Verification key for database-backed OAuth and SAML relay state.
|
|
39
|
-
*/
|
|
40
|
-
const getAuthStateVerificationIdentifier = (state) => `auth-state:${state}`;
|
|
41
38
|
const INTERNAL_STATE_KEYS = new Set(Object.keys(stateDataSchema.shape));
|
|
42
39
|
var StateError = class extends BetterAuthError {
|
|
43
40
|
code;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.21",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -503,13 +503,13 @@
|
|
|
503
503
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
504
504
|
"nanostores": "^1.3.0",
|
|
505
505
|
"zod": "^4.5.4",
|
|
506
|
-
"@better-auth/core": "1.7.
|
|
507
|
-
"@better-auth/drizzle-adapter": "1.7.
|
|
508
|
-
"@better-auth/kysely-adapter": "1.7.
|
|
509
|
-
"@better-auth/memory-adapter": "1.7.
|
|
510
|
-
"@better-auth/mongo-adapter": "1.7.
|
|
511
|
-
"@better-auth/prisma-adapter": "1.7.
|
|
512
|
-
"@better-auth/telemetry": "1.7.
|
|
506
|
+
"@better-auth/core": "1.7.21",
|
|
507
|
+
"@better-auth/drizzle-adapter": "1.7.21",
|
|
508
|
+
"@better-auth/kysely-adapter": "1.7.21",
|
|
509
|
+
"@better-auth/memory-adapter": "1.7.21",
|
|
510
|
+
"@better-auth/mongo-adapter": "1.7.21",
|
|
511
|
+
"@better-auth/prisma-adapter": "1.7.21",
|
|
512
|
+
"@better-auth/telemetry": "1.7.21"
|
|
513
513
|
},
|
|
514
514
|
"devDependencies": {
|
|
515
515
|
"@lynx-js/react": "^0.121.2",
|