docs/content/docs/plugins/2fa.mdx
OTP TOTP Backup Codes Trusted Devices
Two-Factor Authentication (2FA) adds an extra security step when users log in. Instead of just using a password, they'll need to provide a second form of verification. This makes it much harder for unauthorized people to access accounts, even if they've somehow gotten the password.
This plugin offers two main methods to do a second factor verification:
Additional features include:
Add the two-factor plugin to your auth configuration and specify your app name as the issuer.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { twoFactor } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ... other config options
appName: "My App", // provide your app name. It'll be used as an issuer. // [!code highlight]
plugins: [
twoFactor() // [!code highlight]
]
})
```
Run the migration or generate the schema to add the necessary fields and tables to the database.
<Tabs items={["migrate", "generate"]}>
<Tab value="migrate">
```package-install
npx auth migrate
```
</Tab>
<Tab value="generate">
```package-install
npx auth generate
```
</Tab>
</Tabs>
See the [Schema](#schema) section to add the fields manually.
Add the client plugin and Specify where the user should be redirected if they need to verify 2nd factor
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [
twoFactorClient() // [!code highlight]
]
})
```
To enable two-factor authentication, call twoFactor.enable with the user's password (required for credential accounts) and issuer (optional). If you enable allowPasswordless, the password can be omitted for users without a credential account.
When 2FA is enabled:
secret and backupCodes are generated.enable returns totpURI and backupCodes.Note: twoFactorEnabled won’t be set to true until the user verifies their TOTP code. Learn more about verifying TOTP in the TOTP section. You can skip verification by setting skipVerificationOnEnable to true in your plugin config.
When a user with 2FA enabled tries to sign in via email, the response object will contain twoFactorRedirect set to true and twoFactorMethods — an array of the 2FA methods available for the user (e.g. ["totp"], ["totp", "otp"]). Use twoFactorMethods to decide which verification UI to show.
You can handle this in the onSuccess callback or by providing a onTwoFactorRedirect callback in the plugin config.
import { authClient } from "@/lib/auth-client"
await authClient.signIn.email({
email: "[email protected]",
password: "password123",
},
{
async onSuccess(context) {
if (context.data.twoFactorRedirect) {
const methods = context.data.twoFactorMethods // e.g. ["totp", "otp"]
// Show the appropriate 2FA verification UI based on available methods
}
},
}
)
Using the onTwoFactorRedirect config:
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [
twoFactorClient({
onTwoFactorRedirect({ twoFactorMethods }){
// twoFactorMethods is e.g. ["totp", "otp"]
// Handle the 2FA verification globally
},
}),
],
});
Using the twoFactorPage config:
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [
twoFactorClient({
twoFactorPage: "/two-factor", // the page to redirect if a user needs to verify their 2nd factor
}),
],
});
When you call auth.api.signInEmail on the server, and the user has 2FA enabled, it will return an object where twoFactorRedirect is set to true. This behavior isn’t inferred in TypeScript, which can be misleading. You can check using in instead to check if twoFactorRedirect is set to true.
authClient.twoFactor.* handles cookies automatically in the browser. If you continue the 2FA flow with auth.api.* on the server, you must pass the relevant headers so Better Auth can read the current 2FA state and set the resulting 2FA/session cookies. The generated examples below use await headers() for this, but in other frameworks you should pass the equivalent incoming request headers.
If you chain multiple auth.api.* calls in the same server flow, make sure you forward the cookies from the previous auth response into the next call.
import { auth } from "@/lib/auth"
const { headers: responseHeaders, response } = await auth.api.signInEmail({
returnHeaders: true,
body: {
email: "[email protected]",
password: "test",
},
});
if ("twoFactorRedirect" in response) {
// response.twoFactorMethods is e.g. ["totp", "otp"]
// Forward the cookies from responseHeaders into the next auth.api 2FA call.
// Handle the 2FA verification in place
}
To disable two-factor authentication, call twoFactor.disable with the user's password (required for credential accounts). If you enable allowPasswordless, the password can be omitted for users without a credential account.
TOTP (Time-Based One-Time Password) is an algorithm that generates a unique password for each login attempt using time as a counter. Every fixed interval (Better Auth defaults to 30 seconds), a new password is generated. This addresses several issues with traditional passwords: they can be forgotten, stolen, or guessed. OTPs solve some of these problems, but their delivery via SMS or email can be unreliable (or even risky, considering it opens new attack vectors).
TOTP, however, generates codes offline, making it both secure and convenient. You just need an authenticator app on your phone.
After enabling 2FA, you can get the TOTP URI to display to the user. This URI is generated by the server using the secret and issuer and can be used to generate a QR code for the user to scan with their authenticator app.
Example: Using React
Once you have the TOTP URI, you can use it to generate a QR code for the user to scan with their authenticator app.
import { authClient } from "@/lib/auth-client"
import QRCode from "react-qr-code";
export default function UserCard({ password }: { password: string }){
const { data: session } = authClient.useSession();
const { data: qr } = useQuery({
queryKey: ["two-factor-qr"],
queryFn: async () => {
const res = await authClient.twoFactor.getTotpUri({ password });
return res.data;
},
enabled: !!session?.user.twoFactorEnabled,
});
return (
<QRCode value={qr?.totpURI || ""} />
)
}
After the user has entered their 2FA code, you can verify it using twoFactor.verifyTotp method. Better Auth follows standard practice by accepting TOTP codes from one period before and one after the current code, ensuring users can authenticate even with minor time delays on their end.
OTP (One-Time Password) is similar to TOTP but a random code is generated and sent to the user's email or phone.
Before using OTP to verify the second factor, you need to configure sendOTP in your Better Auth instance. This function is responsible for sending the OTP to the user's email, phone, or any other method supported by your application.
import { betterAuth } from "better-auth"
import { twoFactor } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
twoFactor({
otpOptions: {
async sendOTP({ user, otp }, ctx) {
// send otp to user
},
},
})
]
})
Sending an OTP is done by calling the authClient.twoFactor.sendOtp function on the client or auth.api.sendTwoFactorOTP on the server. This function will trigger your sendOTP implementation that you provided in the Better Auth configuration.
if (data) { // redirect or show the user to enter the code }
</APIMethod>
#### Verifying OTP
After the user has entered their OTP code, you can verify it using `authClient.twoFactor.verifyOtp` on the client or `auth.api.verifyTwoFactorOTP` on the server.
<APIMethod path="/two-factor/verify-otp" method="POST" requireHeaders headersComment="Pass the current request headers so Better Auth can read and set the 2FA/session cookies.">
```ts
type verifyTwoFactorOTP = {
/**
* The otp code to verify.
*/
code: string = "012345"
/**
* If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
*/
trustDevice?: boolean = true
}
Backup codes are generated and stored in the database. This can be used to recover access to the account if the user loses access to their phone or email.
Generate backup codes for account recovery:
<APIMethod path="/two-factor/generate-backup-codes" method="POST" requireSession> ```ts type generateBackupCodes = { /** * The users password (required for credential accounts). */ password?: string }if (data) { // Show the backup codes to the user }
</APIMethod>
<Callout type="warn">
When you generate backup codes, the old backup codes will be deleted and new ones will be generated.
</Callout>
#### Using Backup Codes
You can now allow users to provide a backup code as an account recovery method.
<APIMethod path="/two-factor/verify-backup-code" method="POST" requireHeaders headersComment="Pass the current request headers so Better Auth can read and set the 2FA/session cookies.">
```ts
type verifyBackupCode = {
/**
* A backup code to verify.
*/
code: string = "123456"
/**
* If true, the session cookie will not be set.
*/
disableSession?: boolean = false
/**
* If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
*/
trustDevice?: boolean = true
}
To display the backup codes to the user, you can call viewBackupCodes on the server. This will return the backup codes in the response. You should only do this if the user has a fresh session - a session that was just created.
You can mark a device as trusted by passing trustDevice to verifyTotp or verifyOtp.
const verify2FA = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyTotp({
code,
trustDevice: true, // Mark this device as trusted // [!code highlight]
})
if (data) {
// 2FA verified and device trusted
}
}
When trustDevice is set to true, the current device will be remembered for 30 days. During this period, the user won't be prompted for 2FA on subsequent sign-ins from this device. The trust period is refreshed each time the user signs in successfully.
By adding an issuer you can set your application name for the 2fa application.
For example, if your user uses Google Auth, the default appName will show up as Better Auth. However, by using the following code, it will show up as my-app-name.
twoFactor({
issuer: "my-app-name" // [!code highlight]
})
The plugin requires 1 additional field in the user table and 1 additional table to store the two factor authentication data.
Table: user
export const twoFactorUserTableFields = [ { name: "twoFactorEnabled", type: "boolean", description: "Whether two factor authentication is enabled for the user.", isOptional: true, }, ];
<DatabaseTable name="user" fields={twoFactorUserTableFields} />Table: twoFactor
export const twoFactorTableFields = [ { name: "id", type: "string", description: "The ID of the two factor authentication.", isPrimaryKey: true, }, { name: "userId", type: "string", description: "The ID of the user", isForeignKey: true, references: { model: "user", field: "id" }, }, { name: "secret", type: "string", description: "The secret used to generate the TOTP code.", }, { name: "backupCodes", type: "string", description: "The backup codes used to recover access to the account if the user loses access to their phone or email.", }, { name: "verified", type: "boolean", description: "Whether this TOTP secret has been verified during enrollment", }, ];
<DatabaseTable name="twoFactor" fields={twoFactorTableFields} />twoFactorTable: The name of the table that stores the two factor authentication data. Default: twoFactor.
skipVerificationOnEnable: Skip the verification process before enabling two factor for a user.
allowPasswordless: Allow enabling and managing 2FA without a password for users that do not have a credential account. Password is still required if a credential account exists.
Issuer: The issuer is the name of your application. It's used to generate TOTP codes. It'll be displayed in the authenticator apps.
TOTP options
these are options for TOTP.
export const twoFactorTotpOptionsType = { digits: { description: "The number of digits the otp to be", type: "number", default: 6, }, period: { description: "The period for totp in seconds.", type: "number", default: 30, }, }
<TypeTable type={twoFactorTotpOptionsType} />OTP options
these are options for OTP.
export const twoFactorOtpOptionsType = { sendOTP: { description: "a function that sends the otp to the user's email or phone number. It takes two parameters: user and otp", type: "function", }, period: { description: "The period for otp in minutes.", type: "number", default: 3, }, storeOTP: { description: "How to transform the stored OTP value, whether plain text, encrypted, or hashed. You can also provide a custom encryptor or hasher. The storage backend is controlled by the global verification config, so secondary storage can be used instead of the database.", type: "string", default: "plain", }, }
<TypeTable type={twoFactorOtpOptionsType} />Backup Code Options
backup codes are generated and stored in the database when the user enabled two factor authentication. This can be used to recover access to the account if the user loses access to their phone or email.
export const twoFactorBackupCodeOptionsType = { amount: { description: "The amount of backup codes to generate", type: "number", default: 10, }, length: { description: "The length of the backup codes", type: "number", default: 10, }, customBackupCodesGenerate: { description: "A function that generates custom backup codes. It takes no parameters and returns an array of strings.", type: "function", }, storeBackupCodes: { description: "How to store the backup codes in the database. Whether to store it as plain text or encrypted. You can also provide a custom encryptor.", type: "string", default: "plain", }, }
<TypeTable type={twoFactorBackupCodeOptionsType} />To use the two factor plugin in the client, you need to add it on your plugins list.
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
const authClient = createAuthClient({
plugins: [
twoFactorClient({ // [!code highlight]
onTwoFactorRedirect({ twoFactorMethods }){ // [!code highlight]
// twoFactorMethods is e.g. ["totp", "otp"] // [!code highlight]
window.location.href = "/2fa" // Handle the 2FA verification redirect // [!code highlight]
} // [!code highlight]
}) // [!code highlight]
]
})
Options
onTwoFactorRedirect: A callback that will be called when the user needs to verify their 2FA code. Receives a context object with twoFactorMethods — an array of enabled 2FA methods (e.g. ["totp", "otp"]). This can be used to redirect the user to the appropriate 2FA page.