Two-factor plugin: two main verification methods
The two-factor plugin offers two main methods for second factor verification: OTP (One-Time Password) — a temporary code sent to email or phone; and TOTP (Time-based One-Time Password) — a code generated by an authenticator app on the user's device.
Two-factor plugin: additional features
The two-factor plugin includes generating backup codes for account recovery, enabling and disabling 2FA, and managing trusted devices.
Two-factor plugin: server installation
To install the two-factor plugin on the server, import twoFactor from 'better-auth/plugins', add it to the plugins array in betterAuth config, and provide an appName which will be used as the TOTP issuer. Then run 'npx auth migrate' or 'npx auth generate' to add the necessary database tables and fields.
Two-factor plugin: client installation
To install the two-factor plugin on the client, import twoFactorClient from 'better-auth/client/plugins' and add it to the plugins array in createAuthClient.
Two-factor enable endpoint: POST /two-factor/enable
The endpoint POST /two-factor/enable accepts the following parameters: password (string, required for email/password accounts), method (string, 'otp' or 'totp', defaults to 'totp'), and issuer (string, custom issuer for TOTP URI, defaults to app name). Requires an active session. When method is 'totp', returns {method: 'totp', totpURI, backupCodes}. When method is 'otp', returns {method: 'otp'}.
Two-factor: TOTP enable defaults to requiring verification
When enabling 2FA with method 'totp', twoFactorEnabled remains false until the user verifies a TOTP code. The server plugin option skipVerificationOnEnable can be set to true to enable TOTP without enrollment-code verification.
Two-factor: OTP enable sets twoFactorEnabled immediately
When enabling 2FA with method 'otp', twoFactorEnabled is set to true immediately. This requires otpOptions.sendOTP to be configured on the server.
Two-factor: password requirement for enabling 2FA
By default, enabling 2FA requires a password. Users who signed up via OAuth, passkeys, magic links, or anonymous auth cannot enable 2FA unless allowPasswordless: true is set in the plugin config. The allowPasswordless option does not change which sign-in methods are challenged for 2FA.
Two-factor: sign-in response with 2FA enabled
When a user with 2FA enabled signs in via email, username, or phone number, the response contains twoFactorRedirect set to true and twoFactorMethods — an array of available 2FA methods for that user (e.g., ['totp'], ['totp', 'otp']).
Two-factor: sign-in endpoints enforcing 2FA
By default, 2FA sign-in enforcement applies to 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.
Two-factor: sign-in with 2FA nulls out newSession
When a 2FA-enabled user signs in via a credential endpoint, the plugin issues a 2FA challenge instead of completing sign-in. 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 reading ctx.context.newSession must null-check it before accessing newSession.user.
Two-factor: handling 2FA redirect with onSuccess callback
When calling authClient.signIn.email with a user who has 2FA enabled, use the onSuccess callback to check context.data.twoFactorRedirect and context.data.twoFactorMethods. If twoFactorRedirect is true, show the appropriate 2FA verification UI based on the available methods.
Two-factor client: twoFactorPage option
The twoFactorClient plugin accepts a twoFactorPage option specifying the page to redirect users to if they need to verify their 2nd factor. Using this option will cause a full page reload when redirecting.
Two-factor: server-side api.signInEmail with 2FA
When calling auth.api.signInEmail on the server and the user has 2FA enabled, the response will have twoFactorRedirect set to true. Check using the 'in' operator to determine if twoFactorRedirect is present. When using auth.api.* calls, pass returnHeaders: true to get response headers, and forward these headers into subsequent auth.api 2FA calls.
Two-factor disable endpoint: POST /two-factor/disable
The endpoint POST /two-factor/disable accepts an optional password parameter (required for credential accounts). Requires an active session. If allowPasswordless is enabled, the password can be omitted for users without a credential account.
TOTP algorithm and defaults in Better Auth
TOTP (Time-Based One-Time Password) is an algorithm that generates a unique password for each login attempt using time as a counter. Better Auth defaults to generating a new password every 30 seconds. TOTP generates codes offline on an authenticator app, making it secure and convenient.
Two-factor get TOTP URI endpoint: POST /two-factor/get-totp-uri
The endpoint POST /two-factor/get-totp-uri accepts an optional password parameter (required for credential accounts). Requires an active session. Returns the TOTP URI that can be used to generate a QR code for the user to scan with their authenticator app.
Two-factor OTP: configuration required before use
Before using OTP to verify the second factor, you must configure sendOTP in the twoFactor plugin options. This function is responsible for sending the OTP to the user's email, phone, or other method. The function signature is async sendOTP({ user, otp }, ctx).
Two-factor send OTP endpoint: POST /two-factor/send-otp
The endpoint POST /two-factor/send-otp accepts an optional trustDevice parameter (boolean, defaults to true, marks device as trusted for 30 days). Requires headers to be passed. Calling this function triggers the sendOTP implementation configured in Better Auth.
Two-factor verify OTP endpoint: POST /two-factor/verify-otp
The endpoint POST /two-factor/verify-otp accepts code (string, the OTP code to verify) and trustDevice (boolean, defaults to true, marks device as trusted for 30 days). Requires headers to be passed for Better Auth to read and set 2FA/session cookies.
Two-factor backup codes: purpose and behavior
Backup codes are generated and stored in the database for account recovery if the user loses access to their phone or email. When backup codes are generated, old backup codes are deleted and new ones are generated. Once a backup code is used, it is removed from the database and cannot be used again.
Two-factor generate backup codes endpoint: POST /two-factor/generate-backup-codes
The endpoint POST /two-factor/generate-backup-codes accepts an optional password parameter (required for credential accounts). Requires an active session. Returns backup codes that should be displayed to the user.
Two-factor verify backup code endpoint: POST /two-factor/verify-backup-code
The endpoint POST /two-factor/verify-backup-code accepts code (string, a backup code to verify), disableSession (boolean, defaults to false, prevents setting session cookie if true), and trustDevice (boolean, defaults to true, marks device as trusted for 30 days). Requires headers to be passed.
Two-factor view backup codes endpoint: POST /two-factor/view-backup-codes
The endpoint POST /two-factor/view-backup-codes is server-only and accepts an optional userId parameter (string or null). Returns the backup codes in the response. Should only be called if the user has a fresh session — a session that was just created.
Two-factor trusted devices: mechanism and duration
A device can be marked as trusted by passing trustDevice: true to verifyTotp or verifyOtp. When trusted, the device is remembered for 30 days, and the user won't be prompted for 2FA on subsequent sign-ins from that device. The trust period is refreshed on each successful sign-in within the 30-day window.
Two-factor TOTP issuer configuration
The issuer for TOTP is set to the app name provided in the auth config, or defaults to 'Better Auth' if not provided. The issuer is displayed in authenticator apps and can be overridden by passing issuer to the plugin config.
Two-factor database schema: user table field
The user table requires one additional field: twoFactorEnabled (boolean, optional) — indicates whether two factor authentication is enabled for the user.
Two-factor database schema: twoFactor table
A twoFactor table is required with the following fields: id (string, primary key, the ID of the two factor authentication), userId (string, foreign key referencing user.id), secret (string, the secret used to generate TOTP code), backupCodes (string, the backup codes for account recovery), verified (boolean, whether TOTP secret has been verified during enrollment), failedVerificationCount (number, consecutive failed second-factor verifications for account lockout), and lockedUntil (date, optional, when account lockout expires; null when not locked).
Two-factor plugin server options: basic configuration
Server options for twoFactor plugin include: twoFactorTable (string, name of table storing 2FA data, defaults to 'twoFactor'), issuer (string, custom issuer name for TOTP URI, defaults to appName), skipVerificationOnEnable (boolean, activate TOTP immediately without verifying enrollment code, defaults to false), and allowPasswordless (boolean, allow enabling/managing 2FA without password for users without credential account, defaults to false).
Two-factor TOTP options configuration
TOTP options include: digits (number, the number of digits in the OTP, defaults to 6) and period (number, the period for TOTP in seconds, defaults to 30).
Two-factor OTP options configuration
OTP options include: sendOTP (function, required, sends OTP to user's email or phone, takes parameters user and otp), period (number, the period for OTP in minutes, defaults to 3), and storeOTP (string, how to store OTP value — plain text, encrypted, or hashed, defaults to 'plain').
Two-factor backup code options configuration
Backup code options include: amount (number, number of backup codes to generate, defaults to 10), length (number, length of each backup code, defaults to 10), customBackupCodesGenerate (function, generates custom backup codes, takes no parameters and returns array of strings), and storeBackupCodes (string, how to store backup codes — plain text or encrypted, defaults to 'plain').
Two-factor account lockout options configuration
Account lockout options include: enabled (boolean, whether account-level lockout is enforced, defaults to true), maxFailedAttempts (number, consecutive failed verifications before lockout, defaults to 10), and durationSeconds (number, how long account stays locked in seconds, defaults to 900). The limit applies per account across sign-in challenges and across factors (TOTP, OTP, and backup codes share one counter). Locked attempts return 429 with ACCOUNT_TEMPORARILY_LOCKED error code.
Two-factor client plugin option: onTwoFactorRedirect
The twoFactorClient plugin accepts an onTwoFactorRedirect option, a callback that receives a context object containing twoFactorMethods (array of enabled 2FA methods, e.g., ['totp', 'otp']). This callback is invoked when the user needs to verify their 2FA code and can be used to redirect or handle 2FA verification.
Two-factor twoFactorPage full page reload warning
Using the twoFactorPage option in twoFactorClient will cause a full page reload when redirecting users to the two-factor authentication page. To avoid page reloads, use the onTwoFactorRedirect callback instead to handle the redirect programmatically.
Two-Factor Authentication plugin available
Better Auth includes a Two-Factor Authentication plugin that enhances app security with two-factor authentication.