BA
Better Auth•2d ago
rdx

Email verification

When signing in with the requiredEmailVerification option on, if the user didn't verify his email address yet, a verification email is automatically sent. This means that the user could spam the sign-in button and/or route Is there any way to prevent that? I guess I could define a custom ratelimit for the sign-in route, as shown here: https://www.better-auth.com/docs/concepts/rate-limit#rate-limit-window but it's not really what I'm looking for. I don't want to ratelimit the route, I just want a cooldown on the verification email
Rate Limit | Better Auth
How to limit the number of requests a user can make to the server in a given time period.
Solution:
I ended up doing exactly that, creating a new column in my db. This is what is looks like: ```tsx const throwInvalidCredsError = (isEmail: boolean) => {...
Jump to solution
4 Replies
Lick A Brick
Lick A Brick•2d ago
An option I can think of is to extend the user table or create a new one and define a column with the datetime of when the verification email was sent. If the datetime is within a certain range of the current datetime you could skip sending the verification email. You can add this check in your function which sends the verification email.
rdx
rdxOP•2d ago
I guess you're right Kinda hoped there was some option/config I didn't know about or something 😦
lonelyplanet
lonelyplanet•2d ago
Not sure if the built in ratelimit covers this or not, But either use a db column like the other guy said inside your send verification email function just add a custom ratelimit
Solution
rdx
rdx•11h ago
I ended up doing exactly that, creating a new column in my db. This is what is looks like:
const throwInvalidCredsError = (isEmail: boolean) => {
throw new APIError("FORBIDDEN", {
code: isEmail
? "INVALID_EMAIL_OR_PASSWORD"
: "INVALID_USERNAME_OR_PASSWORD",
message: isEmail
? "Invalid email or password"
: "Invalid username or password",
})
}

export const beforeHook = createAuthMiddleware(async (ctx) => {
/**
* Before signing-in with email or username, we need to check 2 things:
*
* 1. If the user is trying to sign in using an email or username from a non-credential provider
* 2. If the user is trying to sign in to an account that is not verified and which has a pending verification email
*/
if (ctx.path === "/sign-in/email" || ctx.path === "/sign-in/username") {
const user = await db.user.findFirst({
where: ctx.body.email
? { email: ctx.body.email }
: { username: ctx.body.username },
})

if (!user) {
return
}

const accounts = await db.account.findMany({ where: { userId: user.id } })

// Tried logging in with an email or username from a non-credential provider
if (!accounts.some((a) => a.providerId === "credential")) {
throwInvalidCredsError(ctx.body.email !== undefined)
}

const account = accounts.find((a) => a.providerId === "credential")!
const isCorrectPassword = await ctx.context.password.verify({
password: ctx.body.password,
hash: account.password!,
})

if (!isCorrectPassword) {
throwInvalidCredsError(ctx.body.email !== undefined)
}

// Prevent sending emails too often
if (user.verificationEmailSentAt) {
const lastVerificationSentAt = DateTime.fromJSDate(
user.verificationEmailSentAt,
)
const timeLimit = DateTime.now().minus({ hours: 1 })

if (lastVerificationSentAt > timeLimit) {
throw new APIError("FORBIDDEN", {
code: "VERIFICATION_EMAIL_ALREADY_SENT",
message: "Verification email already sent",
})
}
}

await db.user.update({
where: { id: user.id },
data: { verificationEmailSentAt: new Date() },
})
}
})
const throwInvalidCredsError = (isEmail: boolean) => {
throw new APIError("FORBIDDEN", {
code: isEmail
? "INVALID_EMAIL_OR_PASSWORD"
: "INVALID_USERNAME_OR_PASSWORD",
message: isEmail
? "Invalid email or password"
: "Invalid username or password",
})
}

export const beforeHook = createAuthMiddleware(async (ctx) => {
/**
* Before signing-in with email or username, we need to check 2 things:
*
* 1. If the user is trying to sign in using an email or username from a non-credential provider
* 2. If the user is trying to sign in to an account that is not verified and which has a pending verification email
*/
if (ctx.path === "/sign-in/email" || ctx.path === "/sign-in/username") {
const user = await db.user.findFirst({
where: ctx.body.email
? { email: ctx.body.email }
: { username: ctx.body.username },
})

if (!user) {
return
}

const accounts = await db.account.findMany({ where: { userId: user.id } })

// Tried logging in with an email or username from a non-credential provider
if (!accounts.some((a) => a.providerId === "credential")) {
throwInvalidCredsError(ctx.body.email !== undefined)
}

const account = accounts.find((a) => a.providerId === "credential")!
const isCorrectPassword = await ctx.context.password.verify({
password: ctx.body.password,
hash: account.password!,
})

if (!isCorrectPassword) {
throwInvalidCredsError(ctx.body.email !== undefined)
}

// Prevent sending emails too often
if (user.verificationEmailSentAt) {
const lastVerificationSentAt = DateTime.fromJSDate(
user.verificationEmailSentAt,
)
const timeLimit = DateTime.now().minus({ hours: 1 })

if (lastVerificationSentAt > timeLimit) {
throw new APIError("FORBIDDEN", {
code: "VERIFICATION_EMAIL_ALREADY_SENT",
message: "Verification email already sent",
})
}
}

await db.user.update({
where: { id: user.id },
data: { verificationEmailSentAt: new Date() },
})
}
})

Did you find this page helpful?