better-auth 1.7.5 → 1.7.6
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/routes/callback.mjs +16 -64
- package/dist/api/routes/password.mjs +3 -5
- package/dist/api/routes/sign-in.mjs +2 -0
- package/dist/api/routes/sign-up.mjs +3 -10
- package/dist/api/routes/update-user.mjs +7 -20
- package/dist/client/query-atom.mjs +13 -0
- package/dist/client/query.mjs +37 -27
- package/dist/client/react/react-store.mjs +2 -1
- package/dist/client/session-atom.mjs +2 -1
- package/dist/context/create-context.mjs +3 -2
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/index.mjs +2 -2
- package/dist/oauth2/link-account.d.mts +42 -14
- package/dist/oauth2/link-account.mjs +61 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/admin.mjs +3 -1
- package/dist/plugins/admin/routes.mjs +4 -10
- package/dist/plugins/admin/types.d.mts +4 -1
- package/dist/plugins/captcha/constants.d.mts +1 -0
- package/dist/plugins/captcha/constants.mjs +2 -1
- package/dist/plugins/captcha/index.d.mts +2 -2
- package/dist/plugins/captcha/index.mjs +6 -0
- package/dist/plugins/captcha/types.d.mts +44 -2
- package/dist/plugins/captcha/utils.mjs +11 -1
- package/dist/plugins/captcha/verify-handlers/vercel-botid.mjs +27 -0
- package/dist/plugins/email-otp/routes.mjs +3 -4
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/oauth-proxy/index.mjs +19 -5
- package/dist/plugins/phone-number/routes.mjs +4 -4
- package/dist/plugins/username/index.mjs +2 -0
- package/dist/utils/password.mjs +16 -2
- package/package.json +9 -8
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { isAPIError } from "../../utils/is-api-error.mjs";
|
|
2
2
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
3
|
-
import { assertValidUserInfo } from "../../utils/validate-user-info.mjs";
|
|
4
3
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
5
4
|
import { resolveOAuthAccountKey, toOAuthProfileRecord } from "../../oauth2/account-key.mjs";
|
|
6
5
|
import { OAUTH_CALLBACK_ERROR_CODES, missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
7
|
-
import { getOAuthCallbackPath
|
|
8
|
-
import {
|
|
6
|
+
import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
|
|
7
|
+
import { handleOAuthUserInfo, linkOAuthAccount } from "../../oauth2/link-account.mjs";
|
|
9
8
|
import { generateIdTokenNonce, generateState, parseState } from "../../oauth2/state.mjs";
|
|
10
9
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
11
|
-
import { mergeScopes } from "@better-auth/core/oauth2";
|
|
12
10
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
13
11
|
import { appendQueryParams } from "@better-auth/core/utils/url";
|
|
14
12
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -149,72 +147,26 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
|
|
|
149
147
|
c.context.logger.error("No callback URL found");
|
|
150
148
|
throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.NO_CALLBACK_URL);
|
|
151
149
|
}
|
|
150
|
+
const accountData = {
|
|
151
|
+
...accountKey,
|
|
152
|
+
...tokens,
|
|
153
|
+
scope: tokens.scopes?.join(",")
|
|
154
|
+
};
|
|
152
155
|
if (link) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
method: "oauth",
|
|
163
|
-
oauth: {
|
|
164
|
-
providerId: provider.id,
|
|
165
|
-
profile: providerProfile
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
} catch (e) {
|
|
170
|
-
if (isAPIError(e) && e.body?.code) throw redirectOnError(e.body.code, e.body.message);
|
|
171
|
-
throw e;
|
|
172
|
-
}
|
|
173
|
-
if (!c.context.trustedProviders.includes(provider.id) && !userInfo.emailVerified || c.context.options.account?.accountLinking?.enabled === false) {
|
|
174
|
-
c.context.logger.error("Unable to link account - untrusted provider");
|
|
175
|
-
return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
|
|
176
|
-
}
|
|
177
|
-
if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.EMAIL_DOES_NOT_MATCH);
|
|
178
|
-
const existingAccount = await c.context.internalAdapter.findAccountByKey(accountKey);
|
|
179
|
-
if (existingAccount) {
|
|
180
|
-
if (existingAccount.userId.toString() !== link.userId.toString()) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.ACCOUNT_ALREADY_LINKED_TO_DIFFERENT_USER);
|
|
181
|
-
const mergedScope = mergeScopes(existingAccount.scope, tokens.scopes);
|
|
182
|
-
const updateData = Object.fromEntries(Object.entries({
|
|
183
|
-
providerId: provider.id,
|
|
184
|
-
accessToken: await setTokenUtil(tokens.accessToken, c.context),
|
|
185
|
-
refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
|
|
186
|
-
idToken: tokens.idToken,
|
|
187
|
-
accessTokenExpiresAt: tokens.accessTokenExpiresAt,
|
|
188
|
-
refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
|
|
189
|
-
scope: mergedScope || void 0
|
|
190
|
-
}).filter(([_, value]) => value !== void 0));
|
|
191
|
-
await c.context.internalAdapter.updateAccount(existingAccount.id, updateData);
|
|
192
|
-
} else if (!await c.context.internalAdapter.createAccount({
|
|
193
|
-
userId: link.userId,
|
|
194
|
-
...accountKey,
|
|
195
|
-
...tokens,
|
|
196
|
-
accessToken: await setTokenUtil(tokens.accessToken, c.context),
|
|
197
|
-
refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
|
|
198
|
-
scope: tokens.scopes?.join(",")
|
|
199
|
-
})) return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
|
|
200
|
-
await applyUpdateUserInfoOnLink(c, link.userId, userInfo);
|
|
201
|
-
let toRedirectTo;
|
|
202
|
-
try {
|
|
203
|
-
toRedirectTo = callbackURL.toString();
|
|
204
|
-
} catch {
|
|
205
|
-
toRedirectTo = callbackURL;
|
|
206
|
-
}
|
|
207
|
-
throw c.redirect(toRedirectTo);
|
|
156
|
+
const linkResult = await linkOAuthAccount(c, {
|
|
157
|
+
link,
|
|
158
|
+
userInfo,
|
|
159
|
+
account: accountData,
|
|
160
|
+
profile: providerProfile,
|
|
161
|
+
scopes: tokens.scopes
|
|
162
|
+
});
|
|
163
|
+
if (!linkResult.linked) return redirectOnError(linkResult.error.code, linkResult.error.message);
|
|
164
|
+
throw c.redirect(callbackURL);
|
|
208
165
|
}
|
|
209
166
|
if (!userInfo.email) {
|
|
210
167
|
c.context.logger.error(missingEmailLogMessage(provider.id));
|
|
211
168
|
return redirectOnError(OAUTH_CALLBACK_ERROR_CODES.EMAIL_NOT_FOUND);
|
|
212
169
|
}
|
|
213
|
-
const accountData = {
|
|
214
|
-
...accountKey,
|
|
215
|
-
...tokens,
|
|
216
|
-
scope: tokens.scopes?.join(",")
|
|
217
|
-
};
|
|
218
170
|
let result;
|
|
219
171
|
try {
|
|
220
172
|
result = await handleOAuthUserInfo(c, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { originCheck } from "../middlewares/origin-check.mjs";
|
|
2
2
|
import { getDate } from "../../utils/date.mjs";
|
|
3
3
|
import { sensitiveSessionMiddleware } from "./session.mjs";
|
|
4
|
-
import { validatePassword } from "../../utils/password.mjs";
|
|
4
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort, validatePassword } from "../../utils/password.mjs";
|
|
5
5
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
6
6
|
import { generateId } from "@better-auth/core/utils/id";
|
|
7
7
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -149,10 +149,8 @@ const resetPassword = createAuthEndpoint("/reset-password", {
|
|
|
149
149
|
const token = ctx.body.token || ctx.query?.token;
|
|
150
150
|
if (!token) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
151
151
|
const { newPassword } = ctx.body;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (newPassword.length < minLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
155
|
-
if (newPassword.length > maxLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
152
|
+
assertPasswordNotTooShort(ctx, newPassword);
|
|
153
|
+
assertPasswordNotTooLong(ctx, newPassword);
|
|
156
154
|
const id = `reset-password:${token}`;
|
|
157
155
|
const verification = await ctx.context.internalAdapter.consumeVerificationValue(id);
|
|
158
156
|
if (!verification) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
@@ -9,6 +9,7 @@ import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
|
9
9
|
import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
|
|
10
10
|
import { safeCloneRequest } from "../../utils/request.mjs";
|
|
11
11
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
12
|
+
import { assertPasswordNotTooLong } from "../../utils/password.mjs";
|
|
12
13
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
13
14
|
import { additionalAuthorizationParamsSchema, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
|
|
14
15
|
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
|
|
@@ -312,6 +313,7 @@ const signInEmail = () => createAuthEndpoint("/sign-in/email", {
|
|
|
312
313
|
}
|
|
313
314
|
const { email, password } = ctx.body;
|
|
314
315
|
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
316
|
+
assertPasswordNotTooLong(ctx, password);
|
|
315
317
|
const userRecord = await ctx.context.internalAdapter.findUserByEmail(email.toLowerCase(), { includeAccounts: true });
|
|
316
318
|
const credentialAccount = userRecord?.accounts.find((account) => account.providerId === "credential" && account.accountId === userRecord.user.id);
|
|
317
319
|
if (!userRecord || !credentialAccount) {
|
|
@@ -4,6 +4,7 @@ import { buildSyntheticUserOutput, parseUserInput, parseUserOutput } from "../..
|
|
|
4
4
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
5
|
import { safeCloneRequest } from "../../utils/request.mjs";
|
|
6
6
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
7
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
7
8
|
import { runWithTransaction } from "@better-auth/core/context";
|
|
8
9
|
import { isDevelopment } from "@better-auth/core/env";
|
|
9
10
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
@@ -149,16 +150,8 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
149
150
|
const { name, email, password, image, callbackURL: _callbackURL, rememberMe, ...rest } = body;
|
|
150
151
|
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
151
152
|
if (!password || typeof password !== "string") throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_PASSWORD);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
ctx.context.logger.warn("Password is too short");
|
|
155
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
156
|
-
}
|
|
157
|
-
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
158
|
-
if (password.length > maxPasswordLength) {
|
|
159
|
-
ctx.context.logger.warn("Password is too long");
|
|
160
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
161
|
-
}
|
|
153
|
+
assertPasswordNotTooShort(ctx, password);
|
|
154
|
+
assertPasswordNotTooLong(ctx, password);
|
|
162
155
|
const shouldReturnGenericDuplicateResponse = ctx.context.options.emailAndPassword.requireEmailVerification || ctx.context.options.emailAndPassword.autoSignIn === false;
|
|
163
156
|
const shouldSkipAutoSignIn = ctx.context.options.emailAndPassword.autoSignIn === false || shouldReturnGenericDuplicateResponse;
|
|
164
157
|
const additionalUserFields = parseUserInput(ctx.context.options, rest, "create");
|
|
@@ -4,6 +4,7 @@ import { generateRandomString } from "../../crypto/random.mjs";
|
|
|
4
4
|
import { deleteSessionCookie, setSessionCookie } from "../../cookies/index.mjs";
|
|
5
5
|
import { getSessionFromCtx, isStateful, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
|
|
6
6
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
7
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
7
8
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
8
9
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
9
10
|
import * as z from "zod";
|
|
@@ -158,16 +159,9 @@ const changePassword = createAuthEndpoint("/change-password", {
|
|
|
158
159
|
}, async (ctx) => {
|
|
159
160
|
const { newPassword, currentPassword, revokeOtherSessions } = ctx.body;
|
|
160
161
|
const session = ctx.context.session;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
165
|
-
}
|
|
166
|
-
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
167
|
-
if (newPassword.length > maxPasswordLength) {
|
|
168
|
-
ctx.context.logger.warn("Password is too long");
|
|
169
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
170
|
-
}
|
|
162
|
+
assertPasswordNotTooShort(ctx, newPassword);
|
|
163
|
+
assertPasswordNotTooLong(ctx, newPassword);
|
|
164
|
+
assertPasswordNotTooLong(ctx, currentPassword);
|
|
171
165
|
const account = await ctx.context.internalAdapter.findCredentialAccount(session.user.id);
|
|
172
166
|
if (!account || !account.password) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND);
|
|
173
167
|
const passwordHash = await ctx.context.password.hash(newPassword);
|
|
@@ -203,16 +197,8 @@ newPassword: z.string().meta({ description: "The new password to set is required
|
|
|
203
197
|
}, async (ctx) => {
|
|
204
198
|
const { newPassword } = ctx.body;
|
|
205
199
|
const session = ctx.context.session;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
ctx.context.logger.warn("Password is too short");
|
|
209
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
210
|
-
}
|
|
211
|
-
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
212
|
-
if (newPassword.length > maxPasswordLength) {
|
|
213
|
-
ctx.context.logger.warn("Password is too long");
|
|
214
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
215
|
-
}
|
|
200
|
+
assertPasswordNotTooShort(ctx, newPassword);
|
|
201
|
+
assertPasswordNotTooLong(ctx, newPassword);
|
|
216
202
|
const account = await ctx.context.internalAdapter.findCredentialAccount(session.user.id);
|
|
217
203
|
const passwordHash = await ctx.context.password.hash(newPassword);
|
|
218
204
|
if (!account) {
|
|
@@ -295,6 +281,7 @@ const deleteUser = createAuthEndpoint("/delete-user", {
|
|
|
295
281
|
}
|
|
296
282
|
const session = ctx.context.session;
|
|
297
283
|
if (ctx.body.password) {
|
|
284
|
+
assertPasswordNotTooLong(ctx, ctx.body.password);
|
|
298
285
|
const account = await ctx.context.internalAdapter.findCredentialAccount(session.user.id);
|
|
299
286
|
if (!account || !account.password) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.CREDENTIAL_ACCOUNT_NOT_FOUND);
|
|
300
287
|
if (!await ctx.context.password.verify({
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { atom } from "nanostores";
|
|
2
|
+
//#region src/client/query-atom.ts
|
|
3
|
+
const kAuthQuery = Symbol.for("better-auth:auth-query");
|
|
4
|
+
function isAuthQueryStore(store) {
|
|
5
|
+
return kAuthQuery in store && store[kAuthQuery] === true;
|
|
6
|
+
}
|
|
7
|
+
function createAuthQueryAtom(initialValue) {
|
|
8
|
+
const queryAtom = atom(initialValue);
|
|
9
|
+
Object.defineProperty(queryAtom, kAuthQuery, { value: true });
|
|
10
|
+
return queryAtom;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { createAuthQueryAtom, isAuthQueryStore };
|
package/dist/client/query.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { isJsonEqual, withEquality } from "./equality.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { createAuthQueryAtom } from "./query-atom.mjs";
|
|
3
|
+
import { onMount } from "nanostores";
|
|
3
4
|
//#region src/client/query.ts
|
|
4
5
|
const isServer = () => typeof window === "undefined";
|
|
5
6
|
function isAuthQueryStateEqual(a, b) {
|
|
6
7
|
return isJsonEqual(a.data, b.data) && a.error === b.error && a.isPending === b.isPending && a.isRefetching === b.isRefetching && a.refetch === b.refetch;
|
|
7
8
|
}
|
|
8
9
|
const useAuthQuery = (initializedAtom, path, $fetch, options) => {
|
|
9
|
-
const value =
|
|
10
|
+
const value = createAuthQueryAtom({
|
|
10
11
|
data: null,
|
|
11
12
|
error: null,
|
|
12
13
|
isPending: true,
|
|
@@ -14,7 +15,9 @@ const useAuthQuery = (initializedAtom, path, $fetch, options) => {
|
|
|
14
15
|
refetch: (queryParams) => fn(queryParams)
|
|
15
16
|
});
|
|
16
17
|
onMount(value, () => withEquality(value, isAuthQueryStateEqual));
|
|
18
|
+
let latestRequestId = 0;
|
|
17
19
|
const fn = async (queryParams) => {
|
|
20
|
+
const requestId = ++latestRequestId;
|
|
18
21
|
return new Promise((resolve) => {
|
|
19
22
|
const opts = typeof options === "function" ? options({
|
|
20
23
|
data: value.get().data,
|
|
@@ -28,15 +31,17 @@ const useAuthQuery = (initializedAtom, path, $fetch, options) => {
|
|
|
28
31
|
...queryParams?.query
|
|
29
32
|
},
|
|
30
33
|
async onSuccess(context) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
if (requestId === latestRequestId) {
|
|
35
|
+
const current = value.get();
|
|
36
|
+
const stableData = current.data != null && context.data != null && isJsonEqual(current.data, context.data) ? current.data : context.data;
|
|
37
|
+
value.set({
|
|
38
|
+
data: stableData,
|
|
39
|
+
error: null,
|
|
40
|
+
isPending: false,
|
|
41
|
+
isRefetching: false,
|
|
42
|
+
refetch: value.value.refetch
|
|
43
|
+
});
|
|
44
|
+
}
|
|
40
45
|
await opts?.onSuccess?.(context);
|
|
41
46
|
},
|
|
42
47
|
async onError(context) {
|
|
@@ -44,28 +49,33 @@ const useAuthQuery = (initializedAtom, path, $fetch, options) => {
|
|
|
44
49
|
const retryAttempts = typeof request.retry === "number" ? request.retry : request.retry?.attempts;
|
|
45
50
|
const retryAttempt = request.retryAttempt || 0;
|
|
46
51
|
if (retryAttempts && retryAttempt < retryAttempts) return;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if (requestId === latestRequestId) {
|
|
53
|
+
const isUnauthorized = context.error.status === 401;
|
|
54
|
+
value.set({
|
|
55
|
+
error: context.error,
|
|
56
|
+
data: isUnauthorized ? null : value.get().data,
|
|
57
|
+
isPending: false,
|
|
58
|
+
isRefetching: false,
|
|
59
|
+
refetch: value.value.refetch
|
|
60
|
+
});
|
|
61
|
+
}
|
|
55
62
|
await opts?.onError?.(context);
|
|
56
63
|
},
|
|
57
64
|
async onRequest(context) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
if (requestId === latestRequestId) {
|
|
66
|
+
const currentValue = value.get();
|
|
67
|
+
value.set({
|
|
68
|
+
isPending: currentValue.data === null,
|
|
69
|
+
data: currentValue.data,
|
|
70
|
+
error: null,
|
|
71
|
+
isRefetching: true,
|
|
72
|
+
refetch: value.value.refetch
|
|
73
|
+
});
|
|
74
|
+
}
|
|
66
75
|
await opts?.onRequest?.(context);
|
|
67
76
|
}
|
|
68
77
|
}).catch((error) => {
|
|
78
|
+
if (requestId !== latestRequestId) return;
|
|
69
79
|
value.set({
|
|
70
80
|
error,
|
|
71
81
|
data: value.get().data,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAuthQueryStore } from "../query-atom.mjs";
|
|
1
2
|
import { listenKeys } from "nanostores";
|
|
2
3
|
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
3
4
|
//#region src/client/react/react-store.ts
|
|
@@ -38,7 +39,7 @@ function useStore(store, options = {}) {
|
|
|
38
39
|
return store.listen(emitChange);
|
|
39
40
|
}, deps);
|
|
40
41
|
const get = () => snapshotRef.current;
|
|
41
|
-
return useSyncExternalStore(subscribe, get, get);
|
|
42
|
+
return useSyncExternalStore(subscribe, get, isAuthQueryStore(store) ? () => store.init : get);
|
|
42
43
|
}
|
|
43
44
|
//#endregion
|
|
44
45
|
export { useStore };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isJsonEqual, withEquality } from "./equality.mjs";
|
|
2
|
+
import { createAuthQueryAtom } from "./query-atom.mjs";
|
|
2
3
|
import { createSessionRefreshManager } from "./session-refresh.mjs";
|
|
3
4
|
import { STORE_UNMOUNT_DELAY, atom, onMount } from "nanostores";
|
|
4
5
|
//#region src/client/session-atom.ts
|
|
@@ -44,7 +45,7 @@ function getSessionAtom($fetch, options) {
|
|
|
44
45
|
freshUntil = 0;
|
|
45
46
|
});
|
|
46
47
|
const refetch = (queryParams) => fetchSession(queryParams);
|
|
47
|
-
const session =
|
|
48
|
+
const session = createAuthQueryAtom({
|
|
48
49
|
data: null,
|
|
49
50
|
error: null,
|
|
50
51
|
isPending: true,
|
|
@@ -15,7 +15,7 @@ import { getAuthTables } from "@better-auth/core/db";
|
|
|
15
15
|
import { createLogger, env, isProduction, isTest } from "@better-auth/core/env";
|
|
16
16
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
17
17
|
import { generateId } from "@better-auth/core/utils/id";
|
|
18
|
-
import { schemaCheckFor } from "@better-auth/core/db/internal";
|
|
18
|
+
import { runtimeSchemaCheckFor, schemaCheckFor } from "@better-auth/core/db/internal";
|
|
19
19
|
import { socialProviders } from "@better-auth/core/social-providers";
|
|
20
20
|
import { findInvalidTrustedProxies } from "@better-auth/core/utils/ip";
|
|
21
21
|
import { createTelemetry } from "@better-auth/telemetry";
|
|
@@ -228,7 +228,8 @@ Most of the features of Better Auth will not work correctly.`);
|
|
|
228
228
|
};
|
|
229
229
|
const initOrPromise = runPluginInit(ctx);
|
|
230
230
|
if (isPromise(initOrPromise)) await initOrPromise;
|
|
231
|
-
ctx.checkSchema =
|
|
231
|
+
ctx.checkSchema = runtimeSchemaCheckFor(ctx.adapter);
|
|
232
|
+
ctx.explicitSchemaCheck = schemaCheckFor(ctx.adapter);
|
|
232
233
|
return ctx;
|
|
233
234
|
}
|
|
234
235
|
//#endregion
|
package/dist/oauth2/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GenerateStateOptions, generateIdTokenNonce, generateState, parseState } from "./state.mjs";
|
|
2
|
-
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "./link-account.mjs";
|
|
2
|
+
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo, linkOAuthAccount } from "./link-account.mjs";
|
|
3
3
|
import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "./utils.mjs";
|
|
4
4
|
export * from "@better-auth/core/oauth2";
|
|
5
|
-
export { GenerateStateOptions, applyUpdateUserInfoOnLink, decryptOAuthToken, generateIdTokenNonce, generateState, getOAuthCallbackPath, handleOAuthUserInfo, parseState, setTokenUtil };
|
|
5
|
+
export { GenerateStateOptions, applyUpdateUserInfoOnLink, decryptOAuthToken, generateIdTokenNonce, generateState, getOAuthCallbackPath, handleOAuthUserInfo, linkOAuthAccount, parseState, setTokenUtil };
|
package/dist/oauth2/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "./utils.mjs";
|
|
2
|
-
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "./link-account.mjs";
|
|
2
|
+
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo, linkOAuthAccount } from "./link-account.mjs";
|
|
3
3
|
import { generateIdTokenNonce, generateState, parseState } from "./state.mjs";
|
|
4
4
|
export * from "@better-auth/core/oauth2";
|
|
5
|
-
export { applyUpdateUserInfoOnLink, decryptOAuthToken, generateIdTokenNonce, generateState, getOAuthCallbackPath, handleOAuthUserInfo, parseState, setTokenUtil };
|
|
5
|
+
export { applyUpdateUserInfoOnLink, decryptOAuthToken, generateIdTokenNonce, generateState, getOAuthCallbackPath, handleOAuthUserInfo, linkOAuthAccount, parseState, setTokenUtil };
|
|
@@ -2,9 +2,49 @@ import { Account, User } from "../types/models.mjs";
|
|
|
2
2
|
import { GenericEndpointContext, UserProvisioningSource } from "@better-auth/core";
|
|
3
3
|
|
|
4
4
|
//#region src/oauth2/link-account.d.ts
|
|
5
|
+
type OAuthAccountData = Omit<Account, "id" | "userId" | "createdAt" | "updatedAt">;
|
|
6
|
+
/**
|
|
7
|
+
* Provider profile a freshly linked account may copy onto the local user.
|
|
8
|
+
* `email` and `emailVerified` are identity anchors and are stripped before
|
|
9
|
+
* the remaining fields are written. Provider identity is resolved separately
|
|
10
|
+
* from the raw profile through the provider's account-key contract.
|
|
11
|
+
*/
|
|
12
|
+
type LinkedProviderProfile = {
|
|
13
|
+
name?: string | undefined;
|
|
14
|
+
email?: string | null | undefined;
|
|
15
|
+
emailVerified?: boolean | undefined;
|
|
16
|
+
image?: string | null | undefined;
|
|
17
|
+
};
|
|
18
|
+
interface LinkOAuthAccountOptions {
|
|
19
|
+
link: {
|
|
20
|
+
userId: string;
|
|
21
|
+
email: string;
|
|
22
|
+
};
|
|
23
|
+
userInfo: LinkedProviderProfile;
|
|
24
|
+
account: OAuthAccountData;
|
|
25
|
+
profile: Record<string, unknown>;
|
|
26
|
+
scopes?: string[] | undefined;
|
|
27
|
+
}
|
|
28
|
+
interface LinkOAuthAccountFailure {
|
|
29
|
+
linked: false;
|
|
30
|
+
error: {
|
|
31
|
+
code: string;
|
|
32
|
+
message?: string | undefined;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
type LinkOAuthAccountResult = {
|
|
36
|
+
linked: true;
|
|
37
|
+
} | LinkOAuthAccountFailure;
|
|
38
|
+
declare function linkOAuthAccount(c: GenericEndpointContext, {
|
|
39
|
+
link,
|
|
40
|
+
userInfo,
|
|
41
|
+
account,
|
|
42
|
+
profile,
|
|
43
|
+
scopes
|
|
44
|
+
}: LinkOAuthAccountOptions): Promise<LinkOAuthAccountResult>;
|
|
5
45
|
declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
|
|
6
46
|
userInfo: Omit<User, "createdAt" | "updatedAt">;
|
|
7
|
-
account:
|
|
47
|
+
account: OAuthAccountData;
|
|
8
48
|
callbackURL?: string | undefined;
|
|
9
49
|
disableSignUp?: boolean | undefined;
|
|
10
50
|
overrideUserInfo?: boolean | undefined;
|
|
@@ -67,18 +107,6 @@ declare function handleOAuthUserInfo(c: GenericEndpointContext, opts: {
|
|
|
67
107
|
password?: string | null | undefined;
|
|
68
108
|
} | null;
|
|
69
109
|
}>;
|
|
70
|
-
/**
|
|
71
|
-
* Provider profile a freshly linked account may copy onto the local user.
|
|
72
|
-
* `email` and `emailVerified` are identity anchors and are stripped before
|
|
73
|
-
* the remaining fields are written. Provider identity is resolved separately
|
|
74
|
-
* from the raw profile through the provider's account-key contract.
|
|
75
|
-
*/
|
|
76
|
-
type LinkedProviderProfile = {
|
|
77
|
-
name?: string | undefined;
|
|
78
|
-
email?: string | null | undefined;
|
|
79
|
-
emailVerified?: boolean | undefined;
|
|
80
|
-
image?: string | null | undefined;
|
|
81
|
-
};
|
|
82
110
|
/**
|
|
83
111
|
* Apply the `account.accountLinking.updateUserInfoOnLink` policy: when enabled,
|
|
84
112
|
* copy the freshly linked provider's profile onto the local user, matching the
|
|
@@ -93,4 +121,4 @@ type LinkedProviderProfile = {
|
|
|
93
121
|
*/
|
|
94
122
|
declare function applyUpdateUserInfoOnLink(c: GenericEndpointContext, userId: string, userInfo: LinkedProviderProfile): Promise<User | undefined>;
|
|
95
123
|
//#endregion
|
|
96
|
-
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };
|
|
124
|
+
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo, linkOAuthAccount };
|
|
@@ -8,7 +8,67 @@ import { createEmailVerificationToken } from "../api/routes/email-verification.m
|
|
|
8
8
|
import { queueAfterTransactionHook, runWithTransaction } from "@better-auth/core/context";
|
|
9
9
|
import { isDevelopment } from "@better-auth/core/env";
|
|
10
10
|
import { APIError } from "@better-auth/core/error";
|
|
11
|
+
import { mergeScopes } from "@better-auth/core/oauth2";
|
|
11
12
|
//#region src/oauth2/link-account.ts
|
|
13
|
+
function linkOAuthAccountFailure(code, message) {
|
|
14
|
+
return {
|
|
15
|
+
linked: false,
|
|
16
|
+
error: {
|
|
17
|
+
code,
|
|
18
|
+
message
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function linkOAuthAccount(c, { link, userInfo, account, profile, scopes }) {
|
|
23
|
+
try {
|
|
24
|
+
await assertValidUserInfo(c, {
|
|
25
|
+
user: {
|
|
26
|
+
...userInfo,
|
|
27
|
+
id: link.userId,
|
|
28
|
+
email: userInfo.email ?? void 0
|
|
29
|
+
},
|
|
30
|
+
source: {
|
|
31
|
+
action: "link-account",
|
|
32
|
+
method: "oauth",
|
|
33
|
+
oauth: {
|
|
34
|
+
providerId: account.providerId,
|
|
35
|
+
profile
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (!isAPIError(error) || !error.body?.code) throw error;
|
|
41
|
+
return linkOAuthAccountFailure(error.body.code, error.body.message);
|
|
42
|
+
}
|
|
43
|
+
if (!c.context.trustedProviders.includes(account.providerId) && !userInfo.emailVerified || c.context.options.account?.accountLinking?.enabled === false) {
|
|
44
|
+
c.context.logger.error("Unable to link account - untrusted provider");
|
|
45
|
+
return linkOAuthAccountFailure(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
|
|
46
|
+
}
|
|
47
|
+
if (userInfo.email?.toLowerCase() !== link.email.toLowerCase() && c.context.options.account?.accountLinking?.allowDifferentEmails !== true) return linkOAuthAccountFailure(OAUTH_CALLBACK_ERROR_CODES.EMAIL_DOES_NOT_MATCH);
|
|
48
|
+
const existingAccount = await c.context.internalAdapter.findAccountByKey(account);
|
|
49
|
+
if (existingAccount) {
|
|
50
|
+
if (existingAccount.userId.toString() !== link.userId.toString()) return linkOAuthAccountFailure(OAUTH_CALLBACK_ERROR_CODES.ACCOUNT_ALREADY_LINKED_TO_DIFFERENT_USER);
|
|
51
|
+
const mergedScope = mergeScopes(existingAccount.scope, scopes);
|
|
52
|
+
const updateData = Object.fromEntries(Object.entries({
|
|
53
|
+
providerId: account.providerId,
|
|
54
|
+
accessToken: await setTokenUtil(account.accessToken, c.context),
|
|
55
|
+
refreshToken: await setTokenUtil(account.refreshToken, c.context),
|
|
56
|
+
idToken: account.idToken,
|
|
57
|
+
accessTokenExpiresAt: account.accessTokenExpiresAt,
|
|
58
|
+
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
|
|
59
|
+
scope: mergedScope || void 0
|
|
60
|
+
}).filter(([_, value]) => value !== void 0));
|
|
61
|
+
await c.context.internalAdapter.updateAccount(existingAccount.id, updateData);
|
|
62
|
+
} else if (!await c.context.internalAdapter.createAccount({
|
|
63
|
+
userId: link.userId,
|
|
64
|
+
...account,
|
|
65
|
+
accessToken: await setTokenUtil(account.accessToken, c.context),
|
|
66
|
+
refreshToken: await setTokenUtil(account.refreshToken, c.context),
|
|
67
|
+
scope: scopes?.join(",") ?? account.scope
|
|
68
|
+
})) return linkOAuthAccountFailure(OAUTH_CALLBACK_ERROR_CODES.UNABLE_TO_LINK_ACCOUNT);
|
|
69
|
+
await applyUpdateUserInfoOnLink(c, link.userId, userInfo);
|
|
70
|
+
return { linked: true };
|
|
71
|
+
}
|
|
12
72
|
async function handleOAuthUserInfo(c, opts) {
|
|
13
73
|
const { userInfo, account, callbackURL, disableSignUp, overrideUserInfo } = opts;
|
|
14
74
|
const source = opts.source ?? {
|
|
@@ -326,4 +386,4 @@ async function applyUpdateUserInfoOnLink(c, userId, userInfo) {
|
|
|
326
386
|
}
|
|
327
387
|
}
|
|
328
388
|
//#endregion
|
|
329
|
-
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo };
|
|
389
|
+
export { applyUpdateUserInfoOnLink, handleOAuthUserInfo, linkOAuthAccount };
|
package/dist/package.mjs
CHANGED
|
@@ -42,8 +42,10 @@ const admin = (options) => {
|
|
|
42
42
|
});
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
|
+
const bannedUserMessage = opts.bannedUserMessage;
|
|
46
|
+
const message = typeof bannedUserMessage === "function" ? await bannedUserMessage(user) : bannedUserMessage;
|
|
45
47
|
throw APIError.from("FORBIDDEN", {
|
|
46
|
-
message
|
|
48
|
+
message,
|
|
47
49
|
code: "BANNED_USER"
|
|
48
50
|
});
|
|
49
51
|
}
|
|
@@ -2,6 +2,7 @@ import { parseSessionOutput, parseUserOutput } from "../../db/schema.mjs";
|
|
|
2
2
|
import { getDate } from "../../utils/date.mjs";
|
|
3
3
|
import { deleteSessionCookie, expireCookie, setSessionCookie } from "../../cookies/index.mjs";
|
|
4
4
|
import { getAuthoritativeSessionFromCtx, getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
5
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
5
6
|
import { ADMIN_ERROR_CODES } from "./error-codes.mjs";
|
|
6
7
|
import { hasPermission } from "./has-permission.mjs";
|
|
7
8
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
@@ -190,6 +191,7 @@ const createUser = (opts) => createAuthEndpoint("/admin/create-user", {
|
|
|
190
191
|
}
|
|
191
192
|
const email = ctx.body.email.toLowerCase();
|
|
192
193
|
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
194
|
+
if (ctx.body.password) assertPasswordNotTooLong(ctx, ctx.body.password);
|
|
193
195
|
if (await ctx.context.internalAdapter.findUserByEmail(email)) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
|
|
194
196
|
const user = await ctx.context.internalAdapter.createUser({
|
|
195
197
|
...userData,
|
|
@@ -823,16 +825,8 @@ const setUserPassword = (opts) => createAuthEndpoint("/admin/set-user-password",
|
|
|
823
825
|
permissions: { user: ["set-password"] }
|
|
824
826
|
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD);
|
|
825
827
|
const { newPassword, userId } = ctx.body;
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
ctx.context.logger.warn("Password is too short");
|
|
829
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
830
|
-
}
|
|
831
|
-
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
832
|
-
if (newPassword.length > maxPasswordLength) {
|
|
833
|
-
ctx.context.logger.warn("Password is too long");
|
|
834
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
835
|
-
}
|
|
828
|
+
assertPasswordNotTooShort(ctx, newPassword);
|
|
829
|
+
assertPasswordNotTooLong(ctx, newPassword);
|
|
836
830
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
837
831
|
if (!user) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
838
832
|
const hashedPassword = await ctx.context.password.hash(newPassword);
|
|
@@ -2,6 +2,7 @@ import { Session, User } from "../../types/models.mjs";
|
|
|
2
2
|
import { InferOptionSchema } from "../../types/plugins.mjs";
|
|
3
3
|
import { AccessControl, Role } from "../access/types.mjs";
|
|
4
4
|
import { AdminSchema } from "./schema.mjs";
|
|
5
|
+
import { Awaitable } from "@better-auth/core";
|
|
5
6
|
|
|
6
7
|
//#region src/plugins/admin/types.d.ts
|
|
7
8
|
interface UserWithRole extends User {
|
|
@@ -70,8 +71,10 @@ interface AdminOptions {
|
|
|
70
71
|
* Message to show when a user is banned
|
|
71
72
|
*
|
|
72
73
|
* By default, the message is "You have been banned from this application"
|
|
74
|
+
*
|
|
75
|
+
* Can also be a function that receives the banned user and returns a message.
|
|
73
76
|
*/
|
|
74
|
-
bannedUserMessage?: string | undefined;
|
|
77
|
+
bannedUserMessage?: string | ((user: UserWithRole & Record<string, unknown>) => Awaitable<string>) | undefined;
|
|
75
78
|
/**
|
|
76
79
|
* Whether to allow impersonating other admins.
|
|
77
80
|
*
|
|
@@ -15,7 +15,8 @@ const Providers = {
|
|
|
15
15
|
CLOUDFLARE_TURNSTILE: "cloudflare-turnstile",
|
|
16
16
|
GOOGLE_RECAPTCHA: "google-recaptcha",
|
|
17
17
|
HCAPTCHA: "hcaptcha",
|
|
18
|
-
CAPTCHAFOX: "captchafox"
|
|
18
|
+
CAPTCHAFOX: "captchafox",
|
|
19
|
+
VERCEL_BOTID: "vercel-botid"
|
|
19
20
|
};
|
|
20
21
|
const siteVerifyMap = {
|
|
21
22
|
[Providers.CLOUDFLARE_TURNSTILE]: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseCaptchaOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider } from "./types.mjs";
|
|
1
|
+
import { BaseCaptchaOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, ValidateRequestContext, VercelBotIdOptions } from "./types.mjs";
|
|
2
2
|
import * as _$_better_auth_core0 from "@better-auth/core";
|
|
3
3
|
import * as _$_better_auth_core_utils_error_codes0 from "@better-auth/core/utils/error-codes";
|
|
4
4
|
|
|
@@ -24,4 +24,4 @@ declare const captcha: (options: CaptchaOptions) => {
|
|
|
24
24
|
options: CaptchaOptions;
|
|
25
25
|
};
|
|
26
26
|
//#endregion
|
|
27
|
-
export { BaseCaptchaOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, captcha };
|
|
27
|
+
export { BaseCaptchaOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, ValidateRequestContext, VercelBotIdOptions, captcha };
|
|
@@ -7,6 +7,7 @@ import { captchaFox } from "./verify-handlers/captchafox.mjs";
|
|
|
7
7
|
import { cloudflareTurnstile } from "./verify-handlers/cloudflare-turnstile.mjs";
|
|
8
8
|
import { googleRecaptcha } from "./verify-handlers/google-recaptcha.mjs";
|
|
9
9
|
import { hCaptcha } from "./verify-handlers/h-captcha.mjs";
|
|
10
|
+
import { vercelBotId } from "./verify-handlers/vercel-botid.mjs";
|
|
10
11
|
import { getIP } from "@better-auth/core/utils/ip";
|
|
11
12
|
//#region src/plugins/captcha/index.ts
|
|
12
13
|
const normalizeEndpointPath = (pathname, basePath) => {
|
|
@@ -26,6 +27,11 @@ const captcha = (options) => ({
|
|
|
26
27
|
const basePath = ctx.options.basePath ?? "/api/auth";
|
|
27
28
|
const pathname = normalizeEndpointPath(url.pathname, basePath);
|
|
28
29
|
if (!endpoints.some((endpoint) => endpoint.includes("*") ? wildcardMatch(endpoint)(pathname) : endpoint === pathname)) return;
|
|
30
|
+
if (options.provider === Providers.VERCEL_BOTID) return await vercelBotId({
|
|
31
|
+
request,
|
|
32
|
+
checkBotId: options.checkBotId,
|
|
33
|
+
validateRequest: options.validateRequest
|
|
34
|
+
});
|
|
29
35
|
if (!options.secretKey) throw new Error(INTERNAL_ERROR_CODES.MISSING_SECRET_KEY.message);
|
|
30
36
|
const captchaResponse = request.headers.get("x-captcha-response");
|
|
31
37
|
const remoteUserIP = getIP(request, ctx.options) ?? void 0;
|
|
@@ -47,6 +47,48 @@ interface CaptchaFoxOptions extends BaseCaptchaOptions {
|
|
|
47
47
|
provider: typeof Providers.CAPTCHAFOX;
|
|
48
48
|
siteKey?: string | undefined;
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* BotID verdict fields available to custom request validation.
|
|
52
|
+
*
|
|
53
|
+
* @see https://vercel.com/docs/botid/verified-bots
|
|
54
|
+
*/
|
|
55
|
+
type BotIdVerification = {
|
|
56
|
+
isBot: boolean;
|
|
57
|
+
isVerifiedBot?: boolean | undefined;
|
|
58
|
+
verifiedBotName?: string | undefined;
|
|
59
|
+
verifiedBotCategory?: string | undefined;
|
|
60
|
+
};
|
|
61
|
+
type ValidateRequestContext = {
|
|
62
|
+
request: Request;
|
|
63
|
+
verification: BotIdVerification;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Protect auth requests with Vercel BotID. The browser must also protect the
|
|
67
|
+
* same request paths and methods with BotID's client SDK.
|
|
68
|
+
*
|
|
69
|
+
* @see https://vercel.com/docs/botid/get-started
|
|
70
|
+
*/
|
|
71
|
+
interface VercelBotIdOptions {
|
|
72
|
+
provider: typeof Providers.VERCEL_BOTID;
|
|
73
|
+
/**
|
|
74
|
+
* Auth paths to verify, without the Better Auth base path.
|
|
75
|
+
*
|
|
76
|
+
* @default ["/sign-up/email", "/sign-in/email", "/request-password-reset"]
|
|
77
|
+
*/
|
|
78
|
+
endpoints?: string[] | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Vercel's server-side check for the current request. Pass `checkBotId` from
|
|
81
|
+
* `botid/server`, or wrap it to supply SDK options.
|
|
82
|
+
*/
|
|
83
|
+
checkBotId: () => Promise<BotIdVerification>;
|
|
84
|
+
/**
|
|
85
|
+
* Override the default `isBot === false` decision. Return `true` to allow
|
|
86
|
+
* the request, including a verified bot you trust.
|
|
87
|
+
*
|
|
88
|
+
* @see https://vercel.com/docs/botid/verified-bots
|
|
89
|
+
*/
|
|
90
|
+
validateRequest?: (ctx: ValidateRequestContext) => boolean | Promise<boolean>;
|
|
91
|
+
}
|
|
92
|
+
type CaptchaOptions = GoogleRecaptchaOptions | CloudflareTurnstileOptions | HCaptchaOptions | CaptchaFoxOptions | VercelBotIdOptions;
|
|
51
93
|
//#endregion
|
|
52
|
-
export { BaseCaptchaOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider };
|
|
94
|
+
export { BaseCaptchaOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, ValidateRequestContext, VercelBotIdOptions };
|
|
@@ -5,5 +5,15 @@ const encodeToURLParams = (obj) => {
|
|
|
5
5
|
for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) params.append(key, String(value));
|
|
6
6
|
return params.toString();
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* Rejects after `timeoutMs` without cancelling the underlying promise.
|
|
10
|
+
*/
|
|
11
|
+
const withTimeout = (promise, timeoutMs) => {
|
|
12
|
+
let timer;
|
|
13
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
14
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Operation timed out")), timeoutMs);
|
|
15
|
+
});
|
|
16
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
17
|
+
};
|
|
8
18
|
//#endregion
|
|
9
|
-
export { encodeToURLParams };
|
|
19
|
+
export { encodeToURLParams, withTimeout };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { middlewareResponse } from "../../../utils/middleware-response.mjs";
|
|
2
|
+
import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
|
|
3
|
+
import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
|
|
4
|
+
import { withTimeout } from "../utils.mjs";
|
|
5
|
+
//#region src/plugins/captcha/verify-handlers/vercel-botid.ts
|
|
6
|
+
const vercelBotId = async ({ request, checkBotId, validateRequest }) => {
|
|
7
|
+
let isValid;
|
|
8
|
+
try {
|
|
9
|
+
isValid = await withTimeout((async () => {
|
|
10
|
+
const verification = await checkBotId();
|
|
11
|
+
return validateRequest ? validateRequest({
|
|
12
|
+
request,
|
|
13
|
+
verification
|
|
14
|
+
}) : verification.isBot === false;
|
|
15
|
+
})(), CAPTCHA_VERIFY_TIMEOUT_MS);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message, { cause: error });
|
|
18
|
+
}
|
|
19
|
+
if (isValid) return void 0;
|
|
20
|
+
return middlewareResponse({
|
|
21
|
+
message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
|
|
22
|
+
code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
|
|
23
|
+
status: 403
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { vercelBotId };
|
|
@@ -6,6 +6,7 @@ import { symmetricDecrypt } from "../../crypto/index.mjs";
|
|
|
6
6
|
import { setCookieCache, setSessionCookie } from "../../cookies/index.mjs";
|
|
7
7
|
import { revokeUnprovenAccountAccess } from "../../db/revoke-unproven-account-access.mjs";
|
|
8
8
|
import { getSessionFromCtx, sensitiveSessionMiddleware } from "../../api/routes/session.mjs";
|
|
9
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
9
10
|
import { APIError as APIError$1 } from "../../api/index.mjs";
|
|
10
11
|
import { EMAIL_OTP_ERROR_CODES } from "./error-codes.mjs";
|
|
11
12
|
import { splitAtLastColon, toOTPIdentifier } from "./utils.mjs";
|
|
@@ -581,10 +582,8 @@ const resetPasswordEmailOTP = (opts) => createAuthEndpoint("/email-otp/reset-pas
|
|
|
581
582
|
} }
|
|
582
583
|
}, async (ctx) => {
|
|
583
584
|
const email = ctx.body.email.toLowerCase();
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
587
|
-
if (ctx.body.password.length > maxPasswordLength) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
585
|
+
assertPasswordNotTooShort(ctx, ctx.body.password);
|
|
586
|
+
assertPasswordNotTooLong(ctx, ctx.body.password);
|
|
588
587
|
await atomicVerifyOTP(ctx, opts, toOTPIdentifier("forget-password", email), ctx.body.otp);
|
|
589
588
|
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
590
589
|
if (!user) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -10,7 +10,7 @@ import { admin } from "./admin/admin.mjs";
|
|
|
10
10
|
import { AnonymousOptions, AnonymousSession, UserWithAnonymous } from "./anonymous/types.mjs";
|
|
11
11
|
import { anonymous } from "./anonymous/index.mjs";
|
|
12
12
|
import { BearerOptions, bearer } from "./bearer/index.mjs";
|
|
13
|
-
import { BaseCaptchaOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider } from "./captcha/types.mjs";
|
|
13
|
+
import { BaseCaptchaOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, ValidateRequestContext, VercelBotIdOptions } from "./captcha/types.mjs";
|
|
14
14
|
import { captcha } from "./captcha/index.mjs";
|
|
15
15
|
import { CustomSessionPluginOptions, customSession } from "./custom-session/index.mjs";
|
|
16
16
|
import { TimeString, ms, sec } from "../utils/time.mjs";
|
|
@@ -66,4 +66,4 @@ import { USERNAME_ERROR_CODES } from "./username/error-codes.mjs";
|
|
|
66
66
|
import { UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, username } from "./username/index.mjs";
|
|
67
67
|
import { hasPermission } from "./organization/has-permission.mjs";
|
|
68
68
|
import { DefaultOrganizationPlugin, DynamicAccessControlEndpoints, OrganizationCreator, OrganizationEndpoints, OrganizationPlugin, TeamEndpoints, organization, parseRoles } from "./organization/organization.mjs";
|
|
69
|
-
export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationGrant, DeviceAuthorizationGrantAuthorization, DeviceAuthorizationOptions, DeviceAuthorizationPluginOptions, DeviceAuthorizationRequest, DeviceCode, DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GenericOAuthUserInfo, GoogleRecaptchaOptions, GumroadOptions, HCaptchaOptions, HIDE_METADATA, HaveIBeenPwnedOptions, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOptionSchema, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginContext, InferPluginErrorCodes, InferPluginIDs, InferTeam, Invitation, InvitationInput, InvitationStatus, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodOptions, LineOptions, LoginResult, MagicLinkOptions, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, OAuthPopupData, OAuthPopupMessage, OAuthProxyOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, POPUP_MARKER_COOKIE, Path, PatreonOptions, PhoneNumberOptions, Provider, ResolvedSigningKey, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestAuthOptions, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, YandexOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getJwtToken, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, isPasswordCompromised, jwt, keycloak, lastLoginMethod, line, magicLink, memberSchema, microsoftEntraId, ms, multiSession, oAuthProxy, oauthPopup, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, redeemDeviceCode, resolveSigningKey, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, yandex };
|
|
69
|
+
export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationGrant, DeviceAuthorizationGrantAuthorization, DeviceAuthorizationOptions, DeviceAuthorizationPluginOptions, DeviceAuthorizationRequest, DeviceCode, DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GenericOAuthUserInfo, GoogleRecaptchaOptions, GumroadOptions, HCaptchaOptions, HIDE_METADATA, HaveIBeenPwnedOptions, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOptionSchema, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginContext, InferPluginErrorCodes, InferPluginIDs, InferTeam, Invitation, InvitationInput, InvitationStatus, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodOptions, LineOptions, LoginResult, MagicLinkOptions, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, OAuthPopupData, OAuthPopupMessage, OAuthProxyOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, POPUP_MARKER_COOKIE, Path, PatreonOptions, PhoneNumberOptions, Provider, ResolvedSigningKey, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestAuthOptions, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, ValidateRequestContext, VercelBotIdOptions, YandexOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getJwtToken, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, isPasswordCompromised, jwt, keycloak, lastLoginMethod, line, magicLink, memberSchema, microsoftEntraId, ms, multiSession, oAuthProxy, oauthPopup, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, redeemDeviceCode, resolveSigningKey, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, yandex };
|
|
@@ -7,7 +7,7 @@ import { setSessionCookie } from "../../cookies/index.mjs";
|
|
|
7
7
|
import { resolveOAuthAccountKey, toOAuthProfileRecord } from "../../oauth2/account-key.mjs";
|
|
8
8
|
import { redirectOnError } from "../../oauth2/errors.mjs";
|
|
9
9
|
import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
|
|
10
|
-
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
10
|
+
import { handleOAuthUserInfo, linkOAuthAccount } from "../../oauth2/link-account.mjs";
|
|
11
11
|
import { setOAuthState } from "../../api/state/oauth.mjs";
|
|
12
12
|
import { parseGenericState } from "../../state.mjs";
|
|
13
13
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
@@ -37,6 +37,7 @@ const passthroughPayloadSchema = z.looseObject({
|
|
|
37
37
|
updatedAt: true
|
|
38
38
|
}).shape),
|
|
39
39
|
profile: z.record(z.string(), z.unknown()).optional(),
|
|
40
|
+
scopes: z.array(z.string()).optional(),
|
|
40
41
|
state: z.string().min(1),
|
|
41
42
|
callbackURL: z.string().min(1),
|
|
42
43
|
newUserURL: z.string().optional(),
|
|
@@ -107,7 +108,19 @@ const oAuthProxy = (opts) => {
|
|
|
107
108
|
ctx.context.logger.error(`OAuth proxy payload expired or invalid (age: ${age}s, maxAge: ${maxAge}s)`);
|
|
108
109
|
throw redirectOnError(ctx, errorURL, "payload_expired");
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
+
const stateData = await restoreOAuthProxyState(ctx, payload.state);
|
|
112
|
+
if (!stateData) throw redirectOnError(ctx, errorURL, "state_mismatch");
|
|
113
|
+
if (stateData.link) {
|
|
114
|
+
const linkResult = await linkOAuthAccount(ctx, {
|
|
115
|
+
link: stateData.link,
|
|
116
|
+
userInfo: payload.userInfo,
|
|
117
|
+
account: payload.account,
|
|
118
|
+
profile: payload.profile ?? {},
|
|
119
|
+
scopes: payload.scopes ?? payload.account.scope?.split(",")
|
|
120
|
+
});
|
|
121
|
+
if (!linkResult.linked) throw redirectOnError(ctx, errorURL, linkResult.error.code, linkResult.error.message);
|
|
122
|
+
throw ctx.redirect(payload.callbackURL);
|
|
123
|
+
}
|
|
111
124
|
let result;
|
|
112
125
|
try {
|
|
113
126
|
result = await handleOAuthUserInfo(ctx, {
|
|
@@ -185,7 +198,7 @@ const oAuthProxy = (opts) => {
|
|
|
185
198
|
hooks: {
|
|
186
199
|
before: [{
|
|
187
200
|
matcher(context) {
|
|
188
|
-
return !!context.path?.startsWith("/sign-in/social");
|
|
201
|
+
return !!context.path?.startsWith("/sign-in/social") || context.path === "/link-social";
|
|
189
202
|
},
|
|
190
203
|
handler: createAuthMiddleware(async (ctx) => {
|
|
191
204
|
if (checkSkipProxy(ctx, opts)) return;
|
|
@@ -307,6 +320,7 @@ const oAuthProxy = (opts) => {
|
|
|
307
320
|
emailVerified: userInfo.emailVerified
|
|
308
321
|
},
|
|
309
322
|
profile: providerProfile,
|
|
323
|
+
scopes: tokens.scopes,
|
|
310
324
|
account: {
|
|
311
325
|
...accountKey,
|
|
312
326
|
accessToken: tokens.accessToken,
|
|
@@ -333,14 +347,14 @@ const oAuthProxy = (opts) => {
|
|
|
333
347
|
}],
|
|
334
348
|
after: [{
|
|
335
349
|
matcher(context) {
|
|
336
|
-
return !!context.path?.startsWith("/sign-in/social");
|
|
350
|
+
return !!context.path?.startsWith("/sign-in/social") || context.path === "/link-social";
|
|
337
351
|
},
|
|
338
352
|
handler: createAuthMiddleware(async (ctx) => {
|
|
339
353
|
if (checkSkipProxy(ctx, opts)) return;
|
|
340
354
|
const signInResponse = ctx.context.returned;
|
|
341
355
|
if (!signInResponse || typeof signInResponse !== "object" || !("url" in signInResponse)) return;
|
|
342
356
|
const { url: providerURL } = signInResponse;
|
|
343
|
-
if (typeof providerURL !== "string") return;
|
|
357
|
+
if (typeof providerURL !== "string" || providerURL.length === 0) return;
|
|
344
358
|
const oauthURL = new URL(providerURL);
|
|
345
359
|
const originalState = oauthURL.searchParams.get("state");
|
|
346
360
|
if (!originalState) return;
|
|
@@ -4,6 +4,7 @@ import { generateRandomString } from "../../crypto/random.mjs";
|
|
|
4
4
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
5
|
import { getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
6
6
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
7
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
7
8
|
import { PHONE_NUMBER_ERROR_CODES } from "./error-codes.mjs";
|
|
8
9
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
9
10
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -54,6 +55,7 @@ const signInPhoneNumber = (opts) => createAuthEndpoint("/sign-in/phone-number",
|
|
|
54
55
|
if (opts.phoneNumberValidator) {
|
|
55
56
|
if (!await opts.phoneNumberValidator(ctx.body.phoneNumber)) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER);
|
|
56
57
|
}
|
|
58
|
+
assertPasswordNotTooLong(ctx, password);
|
|
57
59
|
const user = await ctx.context.adapter.findOne({
|
|
58
60
|
model: "user",
|
|
59
61
|
where: [{
|
|
@@ -466,10 +468,8 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
|
|
|
466
468
|
}]
|
|
467
469
|
});
|
|
468
470
|
if (!user) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.UNEXPECTED_ERROR);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (ctx.body.newPassword.length < minLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
472
|
-
if (ctx.body.newPassword.length > maxLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
471
|
+
assertPasswordNotTooShort(ctx, ctx.body.newPassword);
|
|
472
|
+
assertPasswordNotTooLong(ctx, ctx.body.newPassword);
|
|
473
473
|
const hashedPassword = await ctx.context.password.hash(ctx.body.newPassword);
|
|
474
474
|
if (!await ctx.context.internalAdapter.findCredentialAccount(user.id)) await ctx.context.internalAdapter.createAccount({
|
|
475
475
|
userId: user.id,
|
|
@@ -2,6 +2,7 @@ import { mergeSchema, parseUserOutput } from "../../db/schema.mjs";
|
|
|
2
2
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
3
3
|
import { getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
4
4
|
import { createEmailVerificationToken } from "../../api/routes/email-verification.mjs";
|
|
5
|
+
import { assertPasswordNotTooLong } from "../../utils/password.mjs";
|
|
5
6
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
6
7
|
import { USERNAME_ERROR_CODES } from "./error-codes.mjs";
|
|
7
8
|
import { getSchema } from "./schema.mjs";
|
|
@@ -166,6 +167,7 @@ const usernameImpl = (options, includeDisplayUsername) => {
|
|
|
166
167
|
throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.USERNAME_TOO_LONG);
|
|
167
168
|
}
|
|
168
169
|
if (!await (options?.usernameValidator || defaultUsernameValidator)(username)) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.INVALID_USERNAME);
|
|
170
|
+
assertPasswordNotTooLong(ctx, ctx.body.password);
|
|
169
171
|
const user = await ctx.context.adapter.findOne({
|
|
170
172
|
model: "user",
|
|
171
173
|
where: [{
|
package/dist/utils/password.mjs
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
2
2
|
//#region src/utils/password.ts
|
|
3
|
+
function assertPasswordNotTooShort(ctx, password) {
|
|
4
|
+
if (password.length < ctx.context.password.config.minPasswordLength) {
|
|
5
|
+
ctx.context.logger.warn("Password is too short");
|
|
6
|
+
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function assertPasswordNotTooLong(ctx, password) {
|
|
10
|
+
if (password.length > ctx.context.password.config.maxPasswordLength) {
|
|
11
|
+
ctx.context.logger.warn("Password is too long");
|
|
12
|
+
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
3
15
|
async function validatePassword(ctx, data) {
|
|
16
|
+
assertPasswordNotTooLong(ctx, data.password);
|
|
4
17
|
const credentialAccount = await ctx.context.internalAdapter.findCredentialAccount(data.userId);
|
|
5
18
|
const currentPassword = credentialAccount?.password;
|
|
6
19
|
if (!credentialAccount || !currentPassword) return false;
|
|
@@ -10,9 +23,10 @@ async function validatePassword(ctx, data) {
|
|
|
10
23
|
});
|
|
11
24
|
}
|
|
12
25
|
async function checkPassword(userId, c) {
|
|
26
|
+
const password = c.body.password;
|
|
27
|
+
if (typeof password === "string") assertPasswordNotTooLong(c, password);
|
|
13
28
|
const credentialAccount = await c.context.internalAdapter.findCredentialAccount(userId);
|
|
14
29
|
const currentPassword = credentialAccount?.password;
|
|
15
|
-
const password = c.body.password;
|
|
16
30
|
if (!credentialAccount || !currentPassword || !password) {
|
|
17
31
|
if (password) await c.context.password.hash(password);
|
|
18
32
|
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_PASSWORD);
|
|
@@ -29,4 +43,4 @@ async function shouldRequirePassword(ctx, userId, allowPasswordless) {
|
|
|
29
43
|
return Boolean(credentialAccount?.password);
|
|
30
44
|
}
|
|
31
45
|
//#endregion
|
|
32
|
-
export { checkPassword, shouldRequirePassword, validatePassword };
|
|
46
|
+
export { assertPasswordNotTooLong, assertPasswordNotTooShort, checkPassword, shouldRequirePassword, validatePassword };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.6",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -473,13 +473,13 @@
|
|
|
473
473
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
474
474
|
"nanostores": "^1.3.0",
|
|
475
475
|
"zod": "^4.5.4",
|
|
476
|
-
"@better-auth/core": "1.7.
|
|
477
|
-
"@better-auth/drizzle-adapter": "1.7.
|
|
478
|
-
"@better-auth/kysely-adapter": "1.7.
|
|
479
|
-
"@better-auth/memory-adapter": "1.7.
|
|
480
|
-
"@better-auth/mongo-adapter": "1.7.
|
|
481
|
-
"@better-auth/prisma-adapter": "1.7.
|
|
482
|
-
"@better-auth/telemetry": "1.7.
|
|
476
|
+
"@better-auth/core": "1.7.6",
|
|
477
|
+
"@better-auth/drizzle-adapter": "1.7.6",
|
|
478
|
+
"@better-auth/kysely-adapter": "1.7.6",
|
|
479
|
+
"@better-auth/memory-adapter": "1.7.6",
|
|
480
|
+
"@better-auth/mongo-adapter": "1.7.6",
|
|
481
|
+
"@better-auth/prisma-adapter": "1.7.6",
|
|
482
|
+
"@better-auth/telemetry": "1.7.6"
|
|
483
483
|
},
|
|
484
484
|
"devDependencies": {
|
|
485
485
|
"@lynx-js/react": "^0.121.2",
|
|
@@ -494,6 +494,7 @@
|
|
|
494
494
|
"@types/pg": "^8.20.0",
|
|
495
495
|
"@types/react": "^19.2.17",
|
|
496
496
|
"@types/react-dom": "^19.2.3",
|
|
497
|
+
"botid": "^1.5.10",
|
|
497
498
|
"happy-dom": "^20.8.9",
|
|
498
499
|
"msw": "^2.14.6",
|
|
499
500
|
"next": "^16.3.4",
|