Setup
Add
twoFactor()
plugin to server config with
issuer
Add
twoFactorClient()
plugin to client config
Run
npx @better-auth/cli migrate
Verify: check that
twoFactorSecret
column exists on user table
import
{
betterAuth
}
from
"better-auth"
;
import
{
twoFactor
}
from
"better-auth/plugins"
;
export
const
auth
=
betterAuth
(
{
appName
:
"My App"
,
plugins
:
[
twoFactor
(
{
issuer
:
"My App"
,
}
)
,
]
,
}
)
;
Client-Side Setup
import
{
createAuthClient
}
from
"better-auth/client"
;
import
{
twoFactorClient
}
from
"better-auth/client/plugins"
;
export
const
authClient
=
createAuthClient
(
{
plugins
:
[
twoFactorClient
(
{
onTwoFactorRedirect
(
)
{
window
.
location
.
href
=
"/2fa"
;
}
,
}
)
,
]
,
}
)
;
Enabling 2FA for Users
Requires password verification. Returns TOTP URI (for QR code) and backup codes.
const
enable2FA
=
async
(
password
:
string
)
=>
{
const
{
data
,
error
}
=
await
authClient
.
twoFactor
.
enable
(
{
password
,
}
)
;
if
(
data
)
{
// data.totpURI — generate a QR code from this
// data.backupCodes — display to user
}
}
;
twoFactorEnabled
is not set to
true
until first TOTP verification succeeds. Override with
skipVerificationOnEnable: true
(not recommended).
TOTP (Authenticator App)
Displaying the QR Code
import
QRCode
from
"react-qr-code"
;
const
TotpSetup
=
(
{
totpURI
}
:
{
totpURI
:
string
}
)
=>
{
return
<
QRCode
value
=
{
totpURI
}
/>
;
}
;
Verifying TOTP Codes
Accepts codes from one period before/after current time:
const
verifyTotp
=
async
(
code
:
string
)
=>
{
const
{
data
,
error
}
=
await
authClient
.
twoFactor
.
verifyTotp
(
{
code
,
trustDevice
:
true
,
}
)
;
}
;
TOTP Configuration Options
twoFactor
(
{
totpOptions
:
{
digits
:
6
,
// 6 or 8 digits (default: 6)
period
:
30
,
// Code validity period in seconds (default: 30)
}
,
}
)
;
OTP (Email/SMS)
Configuring OTP Delivery
import
{
betterAuth
}
from
"better-auth"
;
import
{
twoFactor
}
from
"better-auth/plugins"
;
import
{
sendEmail
}
from
"./email"
;
export
const
auth
=
betterAuth
(
{
plugins
:
[
twoFactor
(
{
otpOptions
:
{
sendOTP
:
async
(
{
user
,
otp
}
,
ctx
)
=>
{
await
sendEmail
(
{
to
:
user
.
email
,
subject
:
"Your verification code"
,
text
:
Your code is:
${
otp
}
,
}
)
;
}
,
period
:
5
,
// Code validity in minutes (default: 3)
digits
:
6
,
// Number of digits (default: 6)
allowedAttempts
:
5
,
// Max verification attempts (default: 5)
}
,
}
)
,
]
,
}
)
;
Sending and Verifying OTP
Send:
authClient.twoFactor.sendOtp()
. Verify:
authClient.twoFactor.verifyOtp({ code, trustDevice: true })
.
OTP Storage Security
Configure how OTP codes are stored in the database:
twoFactor
(
{
otpOptions
:
{
storeOTP
:
"encrypted"
,
// Options: "plain", "encrypted", "hashed"
}
,
}
)
;
For custom encryption:
twoFactor
(
{
otpOptions
:
{
storeOTP
:
{
encrypt
:
async
(
token
)
=>
myEncrypt
(
token
)
,
decrypt
:
async
(
token
)
=>
myDecrypt
(
token
)
,
}
,
}
,
}
)
;
Backup Codes
Generated automatically when 2FA is enabled. Each code is single-use.
Displaying Backup Codes
const
BackupCodes
=
(
{
codes
}
:
{
codes
:
string
[
]
}
)
=>
{
return
(
<
div
< p
Save these codes in a secure location: </ p
< ul
{ codes . map ( ( code , i ) => ( < li key = { i }
{ code } </ li
) ) } </ ul
</ div
) ; } ; Regenerating Backup Codes Invalidates all previous codes: const regenerateBackupCodes = async ( password : string ) => { const { data , error } = await authClient . twoFactor . generateBackupCodes ( { password , } ) ; // data.backupCodes contains the new codes } ; Using Backup Codes for Recovery const verifyBackupCode = async ( code : string ) => { const { data , error } = await authClient . twoFactor . verifyBackupCode ( { code , trustDevice : true , } ) ; } ; Backup Code Configuration twoFactor ( { backupCodeOptions : { amount : 10 , // Number of codes to generate (default: 10) length : 10 , // Length of each code (default: 10) storeBackupCodes : "encrypted" , // Options: "plain", "encrypted" } , } ) ; Handling 2FA During Sign-In Response includes twoFactorRedirect: true when 2FA is required: Sign-In Flow Call signIn.email({ email, password }) Check context.data.twoFactorRedirect in onSuccess If true , redirect to /2fa verification page Verify via TOTP, OTP, or backup code Session cookie is created on successful verification const signIn = async ( email : string , password : string ) => { const { data , error } = await authClient . signIn . email ( { email , password } , { onSuccess ( context ) { if ( context . data . twoFactorRedirect ) { window . location . href = "/2fa" ; } } , } ) ; } ; Server-side: check "twoFactorRedirect" in response when using auth.api.signInEmail . Trusted Devices Pass trustDevice: true when verifying. Default trust duration: 30 days ( trustDeviceMaxAge ). Refreshes on each sign-in. Security Considerations Session Management Flow: credentials → session removed → temporary 2FA cookie (10 min default) → verify → session created. twoFactor ( { twoFactorCookieMaxAge : 600 , // 10 minutes in seconds (default) } ) ; Rate Limiting Built-in: 3 requests per 10 seconds for all 2FA endpoints. OTP has additional attempt limiting: twoFactor ( { otpOptions : { allowedAttempts : 5 , // Max attempts per OTP code (default: 5) } , } ) ; Encryption at Rest TOTP secrets: encrypted with auth secret. Backup codes: encrypted by default. OTP: configurable ( "plain" , "encrypted" , "hashed" ). Uses constant-time comparison for verification. 2FA can only be enabled for credential (email/password) accounts. Disabling 2FA Requires password confirmation. Revokes trusted device records: const disable2FA = async ( password : string ) => { const { data , error } = await authClient . twoFactor . disable ( { password , } ) ; } ; Complete Configuration Example import { betterAuth } from "better-auth" ; import { twoFactor } from "better-auth/plugins" ; import { sendEmail } from "./email" ; export const auth = betterAuth ( { appName : "My App" , plugins : [ twoFactor ( { // TOTP settings issuer : "My App" , totpOptions : { digits : 6 , period : 30 , } , // OTP settings otpOptions : { sendOTP : async ( { user , otp } ) => { await sendEmail ( { to : user . email , subject : "Your verification code" , text :
Your code is: ${ otp }, } ) ; } , period : 5 , allowedAttempts : 5 , storeOTP : "encrypted" , } , // Backup code settings backupCodeOptions : { amount : 10 , length : 10 , storeBackupCodes : "encrypted" , } , // Session settings twoFactorCookieMaxAge : 600 , // 10 minutes trustDeviceMaxAge : 30 * 24 * 60 * 60 , // 30 days } ) , ] , } ) ;