fix(better-auth): moved email verification check after password check (#4835)

This commit is contained in:
QuintenStr
2025-09-26 00:33:10 +02:00
committed by GitHub
parent 831dd1d64e
commit 2f0f13404f
2 changed files with 79 additions and 37 deletions

View File

@@ -286,43 +286,6 @@ export const username = (options?: UsernameOptions) => {
}); });
} }
if (
ctx.context.options?.emailAndPassword?.requireEmailVerification &&
!user.emailVerified
) {
if (
!ctx.context.options?.emailVerification?.sendVerificationEmail
) {
throw new APIError("FORBIDDEN", {
message: ERROR_CODES.EMAIL_NOT_VERIFIED,
});
}
if (ctx.context.options?.emailVerification?.sendOnSignIn) {
const token = await createEmailVerificationToken(
ctx.context.secret,
user.email,
undefined,
ctx.context.options.emailVerification?.expiresIn,
);
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${
ctx.body.callbackURL || "/"
}`;
await ctx.context.options.emailVerification.sendVerificationEmail(
{
user: user,
url,
token,
},
ctx.request,
);
}
throw new APIError("FORBIDDEN", {
message: ERROR_CODES.EMAIL_NOT_VERIFIED,
});
}
const account = await ctx.context.adapter.findOne<Account>({ const account = await ctx.context.adapter.findOne<Account>({
model: "account", model: "account",
where: [ where: [
@@ -360,6 +323,44 @@ export const username = (options?: UsernameOptions) => {
message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD, message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
}); });
} }
if (
ctx.context.options?.emailAndPassword?.requireEmailVerification &&
!user.emailVerified
) {
if (
!ctx.context.options?.emailVerification?.sendVerificationEmail
) {
throw new APIError("FORBIDDEN", {
message: ERROR_CODES.EMAIL_NOT_VERIFIED,
});
}
if (ctx.context.options?.emailVerification?.sendOnSignIn) {
const token = await createEmailVerificationToken(
ctx.context.secret,
user.email,
undefined,
ctx.context.options.emailVerification?.expiresIn,
);
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${
ctx.body.callbackURL || "/"
}`;
await ctx.context.options.emailVerification.sendVerificationEmail(
{
user: user,
url,
token,
},
ctx.request,
);
}
throw new APIError("FORBIDDEN", {
message: ERROR_CODES.EMAIL_NOT_VERIFIED,
});
}
const session = await ctx.context.internalAdapter.createSession( const session = await ctx.context.internalAdapter.createSession(
user.id, user.id,
ctx, ctx,

View File

@@ -533,3 +533,44 @@ describe("post normalization flow", async (it) => {
expect(session?.user.displayUsername).toBe("Test Username"); expect(session?.user.displayUsername).toBe("Test Username");
}); });
}); });
describe("username email verification flow (no info leak)", async (it) => {
const { client } = await getTestInstance(
{
emailAndPassword: { enabled: true, requireEmailVerification: true },
plugins: [username()],
},
{
clientOptions: {
plugins: [usernameClient()],
},
},
);
it("returns INVALID_USERNAME_OR_PASSWORD for wrong password even if email is unverified", async () => {
await client.signUp.email({
email: "unverified-user@example.com",
username: "unverified_user",
password: "correct-password",
name: "Unverified User",
});
const res = await client.signIn.username({
username: "unverified_user",
password: "wrong-password",
});
expect(res.error?.status).toBe(401);
expect(res.error?.code).toBe("INVALID_USERNAME_OR_PASSWORD");
});
it("returns EMAIL_NOT_VERIFIED only after a correct password for an unverified user", async () => {
const res = await client.signIn.username({
username: "unverified_user",
password: "correct-password",
});
expect(res.error?.status).toBe(403);
expect(res.error?.code).toBe("EMAIL_NOT_VERIFIED");
});
});