How to Add Two-Factor Authentication
Two-factor authentication (2FA) adds a second layer of security beyond passwords. Even if an attacker steals a password, they cannot access the account without the second factor. This guide covers implementing TOTP-based 2FA with authenticator apps.
Find security issues automatically before attackers do.
Follow These Steps
Install TOTP libraries
Use established libraries for TOTP generation and verification.
npm install otpauth qrcodeGenerate a TOTP secret for the user
Create a unique secret when the user enables 2FA.
import { TOTP, Secret } from 'otpauth'
import QRCode from 'qrcode'
function generateTOTPSetup(userEmail: string) {
const secret = new Secret({ size: 20 })
const totp = new TOTP({
issuer: 'YourApp',
label: userEmail,
algorithm: 'SHA1',
digits: 6,
period: 30,
secret
})
return {
secret: secret.base32,
uri: totp.toString()
}
}Display QR code for authenticator app
Generate a QR code the user can scan with their authenticator app.
// API route to start 2FA enrollment
export async function POST(req: Request) {
const session = await auth()
if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 })
const { secret, uri } = generateTOTPSetup(session.user.email)
// Store secret temporarily (not yet enabled)
await db.update(users)
.set({ totpSecret: encrypt(secret), totpEnabled: false })
.where(eq(users.id, session.user.id))
const qrCodeDataUrl = await QRCode.toDataURL(uri)
return Response.json({ qrCode: qrCodeDataUrl, secret })
}Verify the code and enable 2FA
Require the user to enter a valid code before activating 2FA.
function verifyTOTP(secret: string, token: string): boolean {
const totp = new TOTP({
secret: Secret.fromBase32(secret),
algorithm: 'SHA1',
digits: 6,
period: 30
})
const delta = totp.validate({ token, window: 1 })
return delta !== null
}
// Verify and enable
export async function POST(req: Request) {
const { code } = await req.json()
const user = await getUser(session.user.id)
if (verifyTOTP(decrypt(user.totpSecret), code)) {
await db.update(users).set({ totpEnabled: true }).where(eq(users.id, user.id))
const backupCodes = generateBackupCodes()
return Response.json({ success: true, backupCodes })
}
return Response.json({ error: 'Invalid code' }, { status: 400 })
}Generate backup codes
Provide backup codes in case the user loses access to their authenticator app.
import { randomBytes } from 'crypto'
function generateBackupCodes(count = 10): string[] {
return Array.from({ length: count }, () =>
randomBytes(4).toString('hex').toUpperCase().match(/.{4}/g)!.join('-')
)
}
// Store hashed backup codes
async function storeBackupCodes(userId: string, codes: string[]) {
const hashedCodes = await Promise.all(
codes.map(code => bcrypt.hash(code, 10))
)
await db.update(users).set({ backupCodes: hashedCodes }).where(eq(users.id, userId))
}Add 2FA check to login flow
After password verification, check if 2FA is enabled and require the code.
async function login(email: string, password: string, totpCode?: string) {
const user = await verifyPassword(email, password)
if (user.totpEnabled) {
if (!totpCode) {
return { requires2FA: true } // Tell frontend to show 2FA input
}
if (!verifyTOTP(decrypt(user.totpSecret), totpCode)) {
throw new Error('Invalid 2FA code')
}
}
return createSession(user)
}What You'll Achieve
Your application supports TOTP-based two-factor authentication with authenticator app enrollment, backup codes, and integrated login flow verification.
Common Mistakes to Avoid
Mistake
Storing TOTP secrets in plaintext
Fix
Encrypt TOTP secrets before storing in the database. Use AES-256 encryption with a key from environment variables.
Mistake
Not providing backup codes
Fix
Users lose phones. Without backup codes, they are permanently locked out. Generate and display backup codes when 2FA is first enabled.
Mistake
Not rate limiting 2FA code attempts
Fix
Apply strict rate limiting (5 attempts per 15 minutes) to prevent brute-forcing the 6-digit code.
Frequently Asked Questions
Should I require 2FA for all users?
Offer it to all users but only require it for admin accounts or accounts with elevated privileges. Forcing 2FA on all users can increase support burden.
Which 2FA method is most secure?
Hardware security keys (WebAuthn/FIDO2) are most secure, followed by authenticator apps (TOTP), then SMS. Avoid SMS 2FA when possible due to SIM swapping attacks.
Ready to Secure Your App?
VAS automatically scans your deployed app for the security issues covered in this guide. Get actionable results in minutes.
Start Security Scan