Better AuthBA
Better Auth10mo 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
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:

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() },
    })
  }
})
Was this page helpful?