Step-by-Step Guide
6 steps

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

1

Install TOTP libraries

Use established libraries for TOTP generation and verification.

Code Example
npm install otpauth qrcode
2

Generate a TOTP secret for the user

Create a unique secret when the user enables 2FA.

Code Example
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()
  }
}
3

Display QR code for authenticator app

Generate a QR code the user can scan with their authenticator app.

Code Example
// 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 })
}
4

Verify the code and enable 2FA

Require the user to enter a valid code before activating 2FA.

Code Example
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 })
}
5

Generate backup codes

Provide backup codes in case the user loses access to their authenticator app.

Code Example
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))
}
6

Add 2FA check to login flow

After password verification, check if 2FA is enabled and require the code.

Code Example
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