| 1 | --- |
| 2 | name: two-factor-authentication-best-practices |
| 3 | description: Configure TOTP authenticator apps, send OTP codes via email/SMS, manage backup codes, handle trusted devices, and implement 2FA sign-in flows using Better Auth's twoFactor plugin. Use when users need MFA, multi-factor authentication, authenticator setup, or login security with Better Auth. |
| 4 | --- |
| 5 | |
| 6 | ## Setup |
| 7 | |
| 8 | 1. Add `twoFactor()` plugin to server config with `issuer` |
| 9 | 2. Add `twoFactorClient()` plugin to client config |
| 10 | 3. Run `npx @better-auth/cli migrate` |
| 11 | 4. Verify: check that `twoFactorSecret` column exists on user table |
| 12 | |
| 13 | ```ts |
| 14 | import { betterAuth } |
| 15 | import { twoFactor } |
| 16 | |
| 17 | export const auth = |
| 18 | appName: "My App", |
| 19 | plugins: [ |
| 20 | twoFactor({ |
| 21 | issuer: "My App", |
| 22 | }), |
| 23 | ], |
| 24 | }); |
| 25 | ``` |
| 26 | |
| 27 | ### Client-Side Setup |
| 28 | |
| 29 | ```ts |
| 30 | import { createAuthClient } |
| 31 | import { twoFactorClient } |
| 32 | |
| 33 | export const authClient = |
| 34 | plugins: [ |
| 35 | twoFactorClient({ |
| 36 | onTwoFactorRedirect() { |
| 37 | window.location.href |
| 38 | }, |
| 39 | }), |
| 40 | ], |
| 41 | }); |
| 42 | ``` |
| 43 | |
| 44 | ## Enabling 2FA for Users |
| 45 | |
| 46 | Requires password verification. Returns TOTP URI (for QR code) and backup codes. |
| 47 | |
| 48 | ```ts |
| 49 | const enable2FA = async |
| 50 | const { data, |
| 51 | password, |
| 52 | }); |
| 53 | |
| 54 | if (data) { |
| 55 | // data.totpURI — generate a QR code from this |
| 56 | // data.backupCodes — display to user |
| 57 | } |
| 58 | }; |
| 59 | ``` |
| 60 | |
| 61 | `twoFactorEnabled` is not set to `true` until first TOTP verification succeeds. Override with `skipVerificationOnEnable: true |
| 62 | |
| 63 | ## TOTP (Authenticator App) |
| 64 | |
| 65 | ### Displaying the QR Code |
| 66 | |
| 67 | ```tsx |
| 68 | import QRCode from "react-qr-code |
| 69 | |
| 70 | const TotpSetup = ( |
| 71 | return <QRCode |
| 72 | }; |
| 73 | ``` |
| 74 | |
| 75 | ### Verifying TOTP Codes |
| 76 | |
| 77 | Accepts codes from one period before/after current time: |
| 78 | |
| 79 | ```ts |
| 80 | const verifyTotp = async |
| 81 | const { data, |
| 82 | code, |
| 83 | trustDevice: true, |
| 84 | }); |
| 85 | }; |
| 86 | ``` |
| 87 | |
| 88 | ### TOTP Configuration Options |
| 89 | |
| 90 | ```ts |
| 91 | twoFactor({ |
| 92 | totpOptions: { |
| 93 | digits: 6, // |
| 94 | period: 30, // |
| 95 | }, |
| 96 | }); |
| 97 | ``` |
| 98 | |
| 99 | ## OTP (Email/SMS) |
| 100 | |
| 101 | ### Configuring OTP Delivery |
| 102 | |
| 103 | ```ts |
| 104 | import { betterAuth } |
| 105 | import { twoFactor } |
| 106 | import { sendEmail } |
| 107 | |
| 108 | export const auth = |
| 109 | plugins: [ |
| 110 | twoFactor({ |
| 111 | otpOptions: { |
| 112 | sendOTP: async ( |
| 113 | await sendEmail({ |
| 114 | to: user.email |
| 115 | subject: "Your verification code", |
| 116 | text: `Your code is: ${otp |
| 117 | }); |
| 118 | }, |
| 119 | period: 5, // |
| 120 | digits: 6, // |
| 121 | allowedAttempts: 5, // |
| 122 | }, |
| 123 | }), |
| 124 | ], |
| 125 | }); |
| 126 | ``` |
| 127 | |
| 128 | ### Sending and Verifying OTP |
| 129 | |
| 130 | Send: `authClient.twoFactor.sendOtp()`. Verify: `authClient.twoFactor.verifyOtp({ code, trustDevice: true })`. |
| 131 | |
| 132 | ### OTP Storage Security |
| 133 | |
| 134 | Configure how OTP codes are stored in the database: |
| 135 | |
| 136 | ```ts |
| 137 | twoFactor({ |
| 138 | otpOptions: { |
| 139 | storeOTP: "encrypted", // |
| 140 | }, |
| 141 | }); |
| 142 | ``` |
| 143 | |
| 144 | For custom encryption: |
| 145 | |
| 146 | ```ts |
| 147 | twoFactor({ |
| 148 | otpOptions: { |
| 149 | storeOTP: { |
| 150 | encrypt: async ( |
| 151 | decrypt: async ( |
| 152 | }, |
| 153 | }, |
| 154 | }); |
| 155 | ``` |
| 156 | |
| 157 | ## Backup Codes |
| 158 | |
| 159 | Generated automatically when 2FA is enabled. Each code is single-use. |
| 160 | |
| 161 | ### Displaying Backup Codes |
| 162 | |
| 163 | ```tsx |
| 164 | const BackupCodes = ( |
| 165 | return ( |
| 166 | <div> |
| 167 | <p>Save these codes in a secure location:</p |
| 168 | <ul> |
| 169 | {codes.map(( |
| 170 | <li key={ |
| 171 | ))} |
| 172 | </ul> |
| 173 | </div> |
| 174 | ); |
| 175 | }; |
| 176 | ``` |
| 177 | |
| 178 | ### Regenerating Backup Codes |
| 179 | |
| 180 | Invalidates all previous codes: |
| 181 | |
| 182 | ```ts |
| 183 | const regenerateBackupCodes = async |
| 184 | const { data, |
| 185 | password, |
| 186 | }); |
| 187 | // data.backupCodes contains the new codes |
| 188 | }; |
| 189 | ``` |
| 190 | |
| 191 | ### Using Backup Codes for Recovery |
| 192 | |
| 193 | ```ts |
| 194 | const verifyBackupCode = async |
| 195 | const { data, |
| 196 | code, |
| 197 | trustDevice: true, |
| 198 | }); |
| 199 | }; |
| 200 | ``` |
| 201 | |
| 202 | ### Backup Code Configuration |
| 203 | |
| 204 | ```ts |
| 205 | twoFactor({ |
| 206 | backupCodeOptions: { |
| 207 | amount: 10, // |
| 208 | length: 10, // |
| 209 | storeBackupCodes: "encrypted", // |
| 210 | }, |
| 211 | }); |
| 212 | ``` |
| 213 | |
| 214 | ## Handling 2FA During Sign-In |
| 215 | |
| 216 | Response includes `twoFactorRedirect: true` when 2FA is required: |
| 217 | |
| 218 | ### Sign-In Flow |
| 219 | |
| 220 | 1. Call `signIn.email({ email, password })` |
| 221 | 2. Check `context.data.twoFactorRedirect` in `onSuccess` |
| 222 | 3. If `true`, redirect to `/2fa` verification page |
| 223 | 4. Verify via TOTP, OTP, or backup code |
| 224 | 5. Session cookie is created on successful verification |
| 225 | |
| 226 | ```ts |
| 227 | const signIn = async |
| 228 | const { data, |
| 229 | { email, password |
| 230 | { |
| 231 | onSuccess(context) { |
| 232 | if (context.data |
| 233 | window.location.href |
| 234 | } |
| 235 | }, |
| 236 | } |
| 237 | ); |
| 238 | }; |
| 239 | ``` |
| 240 | |
| 241 | Server-side: check `"twoFactorRedirect" in response` when using `auth.api.signInEmail`. |
| 242 | |
| 243 | ## Trusted Devices |
| 244 | |
| 245 | Pass `trustDevice: true` when verifying. Default trust duration: 30 days (`trustDeviceMaxAge`). Refreshes on each sign-in. |
| 246 | |
| 247 | ## Security Considerations |
| 248 | |
| 249 | ### Session Management |
| 250 | |
| 251 | Flow: credentials → session removed → temporary 2FA cookie (10 min default) → verify → session created. |
| 252 | |
| 253 | ```ts |
| 254 | twoFactor({ |
| 255 | twoFactorCookieMaxAge: 600, // |
| 256 | }); |
| 257 | ``` |
| 258 | |
| 259 | ### Rate Limiting |
| 260 | |
| 261 | Built-in: 3 requests per 10 seconds for all 2FA endpoints. OTP has additional attempt limiting: |
| 262 | |
| 263 | ```ts |
| 264 | twoFactor({ |
| 265 | otpOptions: { |
| 266 | allowedAttempts: 5, // |
| 267 | }, |
| 268 | }); |
| 269 | ``` |
| 270 | |
| 271 | ### Encryption at Rest |
| 272 | |
| 273 | TOTP secrets: encrypted with auth secret. Backup codes: encrypted by default. OTP: configurable (`"plain"`, `"encrypted"`, |
| 274 | |
| 275 | 2FA can only be enabled for credential (email/password) accounts. |
| 276 | |
| 277 | ## Disabling 2FA |
| 278 | |
| 279 | Requires password confirmation. Revokes trusted device records: |
| 280 | |
| 281 | ```ts |
| 282 | const disable2FA = async |
| 283 | const { data, |
| 284 | password, |
| 285 | }); |
| 286 | }; |
| 287 | ``` |
| 288 | |
| 289 | ## Complete Configuration Example |
| 290 | |
| 291 | ```ts |
| 292 | import { betterAuth } |
| 293 | import { twoFactor } |
| 294 | import { sendEmail } |
| 295 | |
| 296 | export const auth = |
| 297 | appName: "My App", |
| 298 | plugins: [ |
| 299 | twoFactor({ |
| 300 | // TOTP settings |
| 301 | issuer: "My App", |
| 302 | totpOptions: { |
| 303 | digits: 6, |
| 304 | period: 30, |
| 305 | }, |
| 306 | // OTP settings |
| 307 | otpOptions: { |
| 308 | sendOTP: async ( |
| 309 | await sendEmail({ |
| 310 | to: user.email |
| 311 | subject: "Your verification code", |
| 312 | text: `Your code is: ${otp |
| 313 | }); |
| 314 | }, |
| 315 | period: 5, |
| 316 | allowedAttempts: 5, |
| 317 | storeOTP: "encrypted", |
| 318 | }, |
| 319 | // Backup code settings |
| 320 | backupCodeOptions: { |
| 321 | amount: 10, |
| 322 | length: 10, |
| 323 | storeBackupCodes: "encrypted", |
| 324 | }, |
| 325 | // Session settings |
| 326 | twoFactorCookieMaxAge: 600, // |
| 327 | trustDeviceMaxAge: 30 * 24 |
| 328 | }), |
| 329 | ], |
| 330 | }); |
| 331 | ``` |