Implementing Two-Factor Authentication in Web Applications

A hands-on guide to adding TOTP-based two-factor authentication to a web app, with full working code.

Why TOTP

Time-based One-Time Passwords (TOTP) are the most widely supported second factor — compatible with Google Authenticator, Authy, and most password managers — and don’t require SMS delivery, which has known interception risks.

Generating a Secret and QR Code

const speakeasy = require('speakeasy');
const qrcode = require('qrcode');

async function setupTwoFactor(user) {
  const secret = speakeasy.generateSecret({
    name: `MyApp (${user.email})`,
  });

  await db.updateUser(user.id, { twoFactorSecret: secret.base32, twoFactorEnabled: false });

  const qrCodeDataUrl = await qrcode.toDataURL(secret.otpauth_url);
  return qrCodeDataUrl;
}

Verifying a Code During Setup

function verifyToken(secret, token) {
  return speakeasy.totp.verify({
    secret,
    encoding: 'base32',
    token,
    window: 1,
  });
}

app.post('/2fa/verify-setup', authenticate, async (req, res) => {
  const user = await db.getUser(req.user.id);
  const isValid = verifyToken(user.twoFactorSecret, req.body.token);

  if (!isValid) return res.status(400).json({ error: 'Invalid code' });

  await db.updateUser(user.id, { twoFactorEnabled: true });
  res.json({ success: true });
});

Requiring 2FA at Login

app.post('/login', async (req, res) => {
  const user = await verifyPassword(req.body.email, req.body.password);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  if (user.twoFactorEnabled) {
    const tempToken = generateShortLivedToken(user.id);
    return res.json({ requiresTwoFactor: true, tempToken });
  }

  res.json({ token: generateSessionToken(user.id) });
});

Backup Codes

Always generate a set of one-time backup codes during 2FA setup, hashed and stored the same way passwords are, so users aren’t permanently locked out if they lose their authenticator device.

Common Mistakes

  • Not rate-limiting the verification endpoint, allowing brute-force attempts against the 6-digit code
  • Storing the TOTP secret in plaintext instead of encrypting it at rest
  • No account recovery path other than support tickets, which doesn’t scale and is a social engineering target

Conclusion

TOTP-based 2FA meaningfully reduces account takeover risk for a relatively small implementation cost. Pair it with backup codes and rate limiting, and treat the setup flow itself as security-sensitive code deserving of careful review.