Email OTP
The Email OTP plugin allows user to sign in, verify their email, or reset their password using a one-time password (OTP) sent to their email address.
Installation
Add the plugin to your auth config
Add the emailOTP plugin to your auth config and implement the sendVerificationOTP() method.
import { betterAuth } from "better-auth"
import { emailOTP } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ... other config options
plugins: [
emailOTP({ // [!code highlight]
async sendVerificationOTP({ email, otp, type }) { // [!code highlight]
if (type === "sign-in") { // [!code highlight]
// Send the OTP for sign in // [!code highlight]
} else if (type === "email-verification") { // [!code highlight]
// Send the OTP for email verification // [!code highlight]
} else { // [!code highlight]
// Send the OTP for password reset // [!code highlight]
} // [!code highlight]
}, // [!code highlight]
}) // [!code highlight]
]
})
Add the client plugin
import { createAuthClient } from "better-auth/client"
import { emailOTPClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [
emailOTPClient() // [!code highlight]
]
})
Usage
Send an OTP
Use the sendVerificationOtp() method to send an OTP to the user's email address.
Check an OTP (optional)
Use the checkVerificationOtp() method to check if an OTP is valid.
Sign In with OTP
To sign in with OTP, use the sendVerificationOtp() method to send a "sign-in" OTP to the user's email address.
Once the user provides the OTP, you can sign in the user using the signIn.emailOtp() method.
If the user is not registered, they'll be automatically registered. Configured additional fields are also accepted for new users. To prevent automatic sign-up, pass disableSignUp as true in the options.
Verify Email with OTP
To verify the user's email address with OTP, use the sendVerificationOtp() method to send an "email-verification" OTP to the user's email address.
Once the user provides the OTP, use the verifyEmail() method to complete email verification.
Reset Password with OTP
To reset the user's password with OTP, use the emailOtp.requestPasswordReset() method to send a "forget-password" OTP to the user's email address.
警告
The /forget-password/email-otp endpoint is deprecated. Please use /email-otp/request-password-reset instead.
Once the user provides the OTP, use the checkVerificationOtp() method to check if it's valid (optional).
Then, use the resetPassword() method to reset the user's password.
Change Email with OTP
To allow users to change their email with OTP, first enable the changeEmail feature, which is disabled by default. Set changeEmail.enabled to true:
import { betterAuth } from "better-auth";
export const auth = betterAuth({
plugins: [
emailOTP({
changeEmail: {
enabled: true, // [!code highlight]
}
})
]
})
By default, when a user requests to change their email, an OTP is sent to the new email address. The email is only updated after the user verifies the new email.
Usage
To change the user's email address with OTP, use the emailOtp.requestEmailChange() method to send a "change-email" OTP to the user's new email address.
Once the user provides the OTP, use the changeEmail() method to change the user's email address.
Confirming with Current Email
For added security, you can require users to confirm the change with an OTP sent to their current email before
sending an OTP to the new email address. To enable this, set changeEmail.verifyCurrentEmail to true in the plugin options.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
plugins: [
emailOTP({
changeEmail: {
enabled: true,
verifyCurrentEmail: true, // [!code highlight]
}
})
]
})
Before requesting the email change, use the sendVerificationOtp() method with type email-verification to send an OTP to the user's email address.
Then, the user can provide the OTP when calling requestEmailChange(). The system will first verify the OTP sent to the current email before sending an OTP to the new email.
Override Default Email Verification
To override the default email verification, pass overrideDefaultEmailVerification: true in the options. This will make the system use an email OTP instead of the default verification link whenever email verification is triggered. In other words, the user will verify their email using an OTP rather than clicking a link.
import { betterAuth } from "better-auth";
import { emailOTP } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
emailOTP({
overrideDefaultEmailVerification: true, // [!code highlight]
async sendVerificationOTP({ email, otp, type }) {
// Implement the sendVerificationOTP method to send the OTP to the user's email address
},
}),
],
});
Options
-
sendVerificationOTP: A function that sends the OTP to the user's email address. The function receives an object with the following properties:email: The user's email address.otp: The OTP to send.type: The type of OTP to send. Can be "sign-in", "email-verification", or "forget-password".
警告
It is recommended to not await the email sending to avoid timing attacks. On serverless platforms, use
waitUntilor similar to ensure the email is sent. -
otpLength: The length of the OTP. Defaults to6. -
expiresIn: The expiry time of the OTP in seconds. Defaults to300seconds.
import { betterAuth } from "better-auth"
import { emailOTP } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
emailOTP({
otpLength: 8,
expiresIn: 600
})
]
})
-
sendVerificationOnSignUp: A boolean value that determines whether to send the OTP when a user signs up. Defaults tofalse. -
disableSignUp: A boolean value that determines whether to prevent automatic sign-up when the user is not registered. Defaults tofalse. -
generateOTP: A function that generates the OTP. Defaults to a random 6-digit number. -
allowedAttempts: The maximum number of attempts allowed for verifying an OTP. Defaults to3. After exceeding this limit, the OTP becomes invalid and the user needs to request a new one.
import { betterAuth } from "better-auth"
import { emailOTP } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
emailOTP({
allowedAttempts: 5, // Allow 5 attempts before invalidating the OTP
expiresIn: 300
})
]
})
When the maximum attempts are exceeded, the verifyOTP, signIn.emailOtp, verifyEmail, and resetPassword methods will return an error with code TOO_MANY_ATTEMPTS.
resendStrategy: Controls what happens when a user requests a new OTP while an existing one is still valid. Defaults to"rotate"."rotate": Always generates a new OTP (default behavior)."reuse": Resends the same OTP and extends its expiry. This prevents multiple valid codes from existing simultaneously when emails are delayed. Only works when the OTP is recoverable (plain,encrypted, or custom encrypt/decrypt). Falls back to"rotate"when the OTP is hashed. If the allowed attempts have been exhausted, a fresh OTP is generated instead of reusing the exhausted one.
import { betterAuth } from "better-auth"
import { emailOTP } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
emailOTP({
resendStrategy: "reuse", // [!code highlight]
async sendVerificationOTP({ email, otp, type }) {
// send the OTP
},
})
]
})
storeOTP: The method used to transform the OTP before it is stored by Better Auth's verification layer, whetherencrypted,hashedorplaintext. Default isplaintext.
Note: This will not affect the OTP sent to the user. It only affects the stored OTP value. The storage backend itself is controlled by the global verification config, so if you configure secondaryStorage, these verification records can live there instead of the database.
Alternatively, you can pass a custom encryptor or hasher to control how the stored OTP value is persisted.
Custom encryptor
emailOTP({
storeOTP: {
encrypt: async (otp) => {
return myCustomEncryptor(otp);
},
decrypt: async (otp) => {
return myCustomDecryptor(otp);
},
}
})
Custom hasher
emailOTP({
storeOTP: {
hash: async (otp) => {
return myCustomHasher(otp);
},
}
})