# Two-Factor Authentication (2FA) (/docs/plugins/2fa)

Enhance your app's security with two-factor authentication.



`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:

1. **OTP (One-Time Password)**: A temporary code sent to the user's email or phone.
2. **TOTP (Time-based One-Time Password)**: A code generated by an app on the user's device.

**Additional features include:**

* Generating backup codes for account recovery
* Enabling/disabling 2FA
* Managing trusted devices

## Installation [#installation]

<Steps>
  <Step>
    ### Add the plugin to your auth config [#add-the-plugin-to-your-auth-config]

    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]
        ]
    })
    ```
  </Step>

  <Step>
    ### Migrate the database [#migrate-the-database]

    Run the migration or generate the schema to add the necessary fields and tables to the database.

    <Tabs items="[&#x22;migrate&#x22;, &#x22;generate&#x22;]">
      <Tab value="migrate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth migrate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>

      <Tab value="generate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth generate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>
    </Tabs>

    See the [Schema](#schema) section to add the fields manually.
  </Step>

  <Step>
    ### Add the client plugin [#add-the-client-plugin]

    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]
        ]
    })
    ```
  </Step>
</Steps>

## Usage [#usage]

### Enabling 2FA [#enabling-2fa]

To enable two-factor authentication, call `twoFactor.enable`. The user must provide their password if they have an email/password account. You can choose between two methods:

* **`totp`** (default) — sets up an authenticator app. The user must verify a TOTP code before 2FA becomes active. Returns `{ method: "totp", totpURI, backupCodes }`.
* **`otp`** — enables email/SMS-based codes immediately, with no extra verification step. Returns `{ method: "otp" }`. Requires `otpOptions.sendOTP` to be configured on the server.

<Callout type="warn">
  By default, enabling 2FA requires a password. Users who signed up via OAuth, passkeys, magic links, or anonymous auth cannot enable 2FA unless you set `allowPasswordless: true` in the plugin config. This option does not change which sign-in methods are challenged for 2FA.
</Callout>

**Endpoint:** `POST /two-factor/enable`

### Client Side

```ts
const { data, error } = await authClient.twoFactor.enable({
    password: "secure-password", // required, The user’s password. Required for email/password accounts.
    method: "totp", // The 2FA method to enable.
    issuer: "my-app-name", // Custom issuer for the TOTP URI. Defaults to the app name in your auth config.
});
```

### Server Side

```ts
const data = await auth.api.enableTwoFactor({
    body: {
        password: "secure-password", // required, The user’s password. Required for email/password accounts.
        method: "totp", // The 2FA method to enable.
        issuer: "my-app-name", // Custom issuer for the TOTP URI. Defaults to the app name in your auth config.
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type enableTwoFactor = {
    /**
     * The user’s password. Required for email/password accounts.
     */
    password: string = "secure-password",
    /**
     * The 2FA method to enable.
     */
    method?: "otp" | "totp" = "totp",
    /**
     * Custom issuer for the TOTP URI. Defaults to the app name in your auth config.
     */
    issuer?: string = "my-app-name"
}
```

When `method` is `"totp"` (default):

* Returns `{ method: "totp", totpURI, backupCodes }`. Use `totpURI` to display a QR code.
* By default, `twoFactorEnabled` remains `false` until the user verifies a TOTP code. When the server plugin is configured with `skipVerificationOnEnable: true`, TOTP is enabled without enrollment-code verification. See [verifying TOTP](#totp).

<Callout>
  Calling `twoFactor.enable` when the user already has a verified TOTP authenticator returns `TOTP_ALREADY_ENABLED`. Disable the current authenticator before starting a new enrollment.
</Callout>

When `method` is `"otp"`:

* Returns `{ method: "otp" }`. `twoFactorEnabled` is set to `true` immediately.
* Requires `otpOptions.sendOTP` to be configured on the server.
* OTP codes are sent via the configured `sendOTP` function at sign-in time. Setting `twoFactorEnabled` directly via a database hook achieves the same result.

### Sign In with 2FA [#sign-in-with-2fa]

When a user with 2FA enabled tries to sign in via email, username, or phone number, 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.

By default, 2FA sign-in enforcement applies to the credential-based sign-in endpoints: `/sign-in/email`, `/sign-in/username`, and `/sign-in/phone-number`. Non-credential sign-in methods such as email OTP, magic link, OAuth/social, passkey, anonymous, and similar passwordless flows are not gated by 2FA by default.

If your app needs to require 2FA for those sign-in methods, add custom hook handling for those endpoints and redirect users into your 2FA verification flow before treating the sign-in as complete.

<Callout type="warn">
  When a 2FA-enabled user signs in via a credential endpoint, the plugin issues a 2FA challenge instead of completing the sign-in. As part of this, the pending session is discarded and `ctx.context.newSession` is reset to `null` — there is no authenticated session until the user verifies the second factor. Server-side hooks (e.g. `after` hooks on the sign-in endpoints) that read `ctx.context.newSession` must null-check it before accessing `newSession.user`, otherwise they will throw while a 2FA challenge is in flight.
</Callout>

You can handle this in the `onSuccess` callback or by providing a `onTwoFactorRedirect` callback in the plugin config.

```tsx
import { authClient } from "@/lib/auth-client"

await authClient.signIn.email({
        email: "user@example.com",
        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:

```ts title="auth-client.ts"
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:

```ts title="sign-in.ts"
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
        }),
    ],
});
```

<Callout type="warn">
  Using the `twoFactorPage` option will cause a full page reload when redirecting users to the two-factor authentication page. If you want to avoid page reloads, consider using the `onTwoFactorRedirect` callback instead to handle the redirect programmatically within your application.
</Callout>

<Callout type="warn">
  **With `auth.api`**

  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.

  ```ts title="sign-in.ts"
  import { auth } from "@/lib/auth"

  const { headers: responseHeaders, response } = await auth.api.signInEmail({
  	returnHeaders: true,
  	body: {
  		email: "test@test.com",
  		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
  }
  ```
</Callout>

### Disabling 2FA [#disabling-2fa]

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.

**Endpoint:** `POST /two-factor/disable`

### Client Side

```ts
const { data, error } = await authClient.twoFactor.disable({
    password, // The user's password (required for credential accounts)
});
```

### Server Side

```ts
const data = await auth.api.disableTwoFactor({
    body: {
        password, // The user's password (required for credential accounts)
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type disableTwoFactor = {
    /**
     * The user's password (required for credential accounts)
     */
    password?: string
}
```

### TOTP [#totp]

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.

#### Getting TOTP URI [#getting-totp-uri]

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.

**Endpoint:** `POST /two-factor/get-totp-uri`

### Client Side

```ts
const { data, error } = await authClient.twoFactor.getTotpUri({
    password, // The user's password (required for credential accounts)
});
```

### Server Side

```ts
const data = await auth.api.getTOTPURI({
    body: {
        password, // The user's password (required for credential accounts)
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type getTOTPURI = {
    /**
     * The user's password (required for credential accounts)
     */
    password?: string
}
```

**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.

```tsx
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 || ""} />
   )
}
```

<Callout>
  By default the issuer for TOTP is set to the app name provided in the auth config or if not provided it will be set to `Better Auth`. You can override this by passing `issuer` to the plugin config.
</Callout>

#### Verifying TOTP [#verifying-totp]

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.

**Endpoint:** `POST /two-factor/verify-totp`

### Client Side

```ts
const { data, error } = await authClient.twoFactor.verifyTotp({
    code: "012345", // required, The otp code to verify.
    trustDevice: true, // If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
});
```

### Server Side

```ts
const data = await auth.api.verifyTOTP({
    body: {
        code: "012345", // required, The otp code to verify.
        trustDevice: true, // If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
    },
    // Pass the current request headers so Better Auth can read and set the 2FA/session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type verifyTOTP = {
    /**
     * 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
}
```

### OTP [#otp]

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.

```ts title="auth.ts"
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 OTP [#sending-otp]

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.

**Endpoint:** `POST /two-factor/send-otp`

### Client Side

```ts
const { data, error } = await authClient.twoFactor.sendOtp({
    trustDevice: true, // If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
});
if (data) {
    // redirect or show the user to enter the code
}
```

### Server Side

```ts
const data = await auth.api.sendTwoFactorOTP({
    body: {
        trustDevice: true, // If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
    },
    // Pass the current request headers so Better Auth can read and set the 2FA/session cookies.
    headers: await headers(),
});
if (data) {
    // redirect or show the user to enter the code
}
```

### Type Definition

```ts
type sendTwoFactorOTP = {
    /**
     * 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
}
```

#### Verifying OTP [#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.

**Endpoint:** `POST /two-factor/verify-otp`

### Client Side

```ts
const { data, error } = await authClient.twoFactor.verifyOtp({
    code: "012345", // required, The otp code to verify.
    trustDevice: true, // If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
});
```

### Server Side

```ts
const data = await auth.api.verifyTwoFactorOTP({
    body: {
        code: "012345", // required, The otp code to verify.
        trustDevice: true, // If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
    },
    // Pass the current request headers so Better Auth can read and set the 2FA/session cookies.
    headers: await headers(),
});
```

### Type Definition

```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 [#backup-codes]

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.

#### Generating Backup Codes [#generating-backup-codes]

Generate backup codes for account recovery:

**Endpoint:** `POST /two-factor/generate-backup-codes`

### Client Side

```ts
const { data, error } = await authClient.twoFactor.generateBackupCodes({
    password, // The users password (required for credential accounts).
});
if (data) {
    // Show the backup codes to the user
}
```

### Server Side

```ts
const data = await auth.api.generateBackupCodes({
    body: {
        password, // The users password (required for credential accounts).
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
if (data) {
    // Show the backup codes to the user
}
```

### Type Definition

```ts
type generateBackupCodes = {
    /**
     * The users password (required for credential accounts). 
     */
    password?: string
}
```

<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 [#using-backup-codes]

You can now allow users to provide a backup code as an account recovery method.

**Endpoint:** `POST /two-factor/verify-backup-code`

### Client Side

```ts
const { data, error } = await authClient.twoFactor.verifyBackupCode({
    code: "123456", // required, A backup code to verify.
    disableSession: false, // If true, the session cookie will not be set.
    trustDevice: true, // If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
});
```

### Server Side

```ts
const data = await auth.api.verifyBackupCode({
    body: {
        code: "123456", // required, A backup code to verify.
        disableSession: false, // If true, the session cookie will not be set.
        trustDevice: true, // If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
    },
    // Pass the current request headers so Better Auth can read and set the 2FA/session cookies.
    headers: await headers(),
});
```

### Type Definition

```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
}
```

<Callout>
  Once a backup code is used, it will be removed from the database and can't be used again.
</Callout>

#### Viewing Backup Codes [#viewing-backup-codes]

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.

**Endpoint:** `POST /two-factor/view-backup-codes`

### Server Side

```ts
const data = await auth.api.viewBackupCodes({
    body: {
        userId: "user-id", // The user ID to view all backup codes.
    },
});
```

### Type Definition

```ts
type viewBackupCodes = {
    /**
     * The user ID to view all backup codes. 
     */
    userId?: string | null = "user-id"
}
```

### Trusted Devices [#trusted-devices]

You can mark a device as trusted by passing `trustDevice` to `verifyTotp` or `verifyOtp`.

```ts
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.

### Issuer [#issuer]

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`.

```ts
twoFactor({
    issuer: "my-app-name" // [!code highlight]
})
```

***

## Schema [#schema]

The plugin requires 1 additional field in the `user` table and 1 additional table to store the two factor authentication data.

Table: `user`



<DatabaseTable name="user" fields="twoFactorUserTableFields" />

Table: `twoFactor`



<DatabaseTable name="twoFactor" fields="twoFactorTableFields" />

## Options [#options]

### Server [#server]

**twoFactorTable**: The name of the table that stores the two factor authentication data. Default: `twoFactor`.

**issuer**: Custom issuer name for the TOTP URI. Defaults to your `appName`.

**skipVerificationOnEnable**: Activate TOTP immediately without verifying an enrollment code. Defaults to `false`.

**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. This option does not change which sign-in methods are challenged for 2FA.

**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.



<TypeTable type="twoFactorTotpOptionsType" />

**OTP options**

these are options for OTP.



<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.



<TypeTable type="twoFactorBackupCodeOptionsType" />

**Account lockout**

After repeated failed verifications during sign-in, the account is temporarily locked. The limit applies per account across sign-in challenges and across factors: TOTP, OTP, and backup codes share one counter, and a successful verification resets it. Locked attempts return `429` with the `ACCOUNT_TEMPORARILY_LOCKED` error code. Enabled by default.



<TypeTable type="twoFactorAccountLockoutOptionsType" />

### Client [#client]

To use the two factor plugin in the client, you need to add it on your plugins list.

```ts title="auth-client.ts"
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.

