Sign-in

Two-factor authentication

Authenticator apps, codes by email, backup codes, and requiring it for some roles.
The account page's security tab, with two-factor's Email me codes and Set up an authenticator app

Better Auth's Two-Factor plugin, added by the module.

Turning it on

Enable it

nuxt.config.ts
export default defineNuxtConfig({
  betterAuthUi: {
    twoFactor: {
      enabled: true,
      // Codes by email as well as authenticator apps (needs emails.send).
      otp: true,
      // Who must have it on: true for everyone, or roles.
      required: ['admin', 'moderator']
    }
  }
})

Run Better Auth's migrations

The plugin adds a twoFactorEnabled column to the user table and a twoFactor table.

How people use it

  • Setting it up, on the account page: scan a QR code, confirm with the app's first code, and save the backup codes. The QR code is drawn in the browser, so the secret never leaves it. With otp, codes can be emailed instead. Users with a password confirm with it; users who only sign in with a provider can set it up too.
  • Signing in then asks for a code on /two-factor: from the app, by email, or a backup code (each works once), with "Trust this device". After signing in, they go on to where they were heading.
  • Too many wrong codes lock the account for a while (Better Auth's own lockout).

Every sign-in is challenged

Better Auth's plugin only challenges password sign-ins, and suggests hooks for the rest. The module adds them, so these are challenged too:

  • social and Generic OAuth sign-ins;
  • social ID tokens and One Tap;
  • sign-in links and codes by email.

They run the plugin's own challenge, so codes, trusted devices and the lockout work the same. Provider sign-ins go to /two-factor instead of signing straight in.

Passkeys aren't challenged. A passkey is already two factors: something you have, plus your device's fingerprint, face or PIN.

Requiring it

required makes two-factor mandatory, for everyone (true) or for some roles (['admin', 'moderator']). Until those users set it up:

  • pages send them to the account page, with a banner saying why;
  • your /api routes, the admin's and Better Auth's admin endpoints answer 403.

Better Auth's other endpoints stay open, since setting two-factor up goes through them. Once it's on, they can't turn it off.

A passkey sign-in counts. A session started with a passkey isn't held back, while one started with a password or a provider still needs two-factor. The module notes passkey sessions in Better Auth's verification table, as the Two-Factor plugin does for trusted devices, so there's no extra table.

Users are matched by their admin roles, so a role change applies on their next request. Guests are never required to use it.

Options

enabled
booleandefault: false
otp
booleandefault: false
Codes by email as well as authenticator apps. Sent through emails.send with the two-factor-code template.
required
boolean | string[]default: false
Who must have it on: true for everyone, or a list of roles.
issuer
string
The name authenticator apps show. Defaults to your Better Auth appName.
trustDeviceMaxAge
numberdefault: 2592000
Seconds "Trust this device" lasts (30 days).

In the admin

  • A shield on the users list for users who have it on, and a "Two-factor off" filter.
  • A badge on each user's page, including "Required, not set up".
  • Lost their device? Staff can turn a user's two-factor off from their page. See Managing users.

Adding the plugin yourself

Leave twoFactor.enabled off, add twoFactor() and twoFactorClient() to your auth configs, and the account page picks it up. The extra challenges and required only come with the module's plugin.