Adding TOTP Two-Factor Authentication to Your App

A
Author
14 Jul, 2026 2 min read 383 views

Time-based one-time passwords (TOTP) are the six-digit codes from apps like Google Authenticator, Microsoft Authenticator or 1Password. They stop most account takeovers that start with a stolen password.

How TOTP works

Your server and the user's app share a secret. Both compute a code from the secret and the current 30-second time window. If the codes match, the user has the secret.

1. Enrolment: create a secret and QR code

import { authenticator } from 'otplib';
import QRCode from 'qrcode';

app.post('/2fa/setup', requireLogin, async (req, res) => {
  const secret = authenticator.generateSecret();
  await db.users.update(req.user.id, { pendingTotpSecret: encrypt(secret) });

  const otpauth = authenticator.keyuri(req.user.email, 'Example App', secret);
  res.json({ qr: await QRCode.toDataURL(otpauth), manualKey: secret });
});

2. Confirm with a first code

app.post('/2fa/confirm', requireLogin, async (req, res) => {
  const user = await db.users.find(req.user.id);
  const secret = decrypt(user.pendingTotpSecret);
  if (!authenticator.verify({ token: req.body.code, secret })) {
    return res.status(400).json({ error: 'That code is not correct. Try the newest code.' });
  }
  const recoveryCodes = generateRecoveryCodes(10);
  await db.users.update(user.id, {
    totpSecret: user.pendingTotpSecret,
    pendingTotpSecret: null,
    recoveryCodeHashes: recoveryCodes.map(hashCode),
  });
  res.json({ recoveryCodes }); // show once; the user must save them
});

Only enable 2FA after the user proves their app works; otherwise a mistyped setup locks them out.

3. Sign-in

  1. Check email and password as usual.
  2. If 2FA is on, create a short-lived "pending" session and ask for the code.
  3. Verify the code (allowing one time-step of clock drift) and only then create the full session.
  4. Reject a code that was already used in the same window.

Security details

  • Encrypt TOTP secrets at rest.
  • Rate-limit code attempts; six digits can be brute-forced without limits.
  • Store recovery codes as hashes and invalidate each after use.
  • Require a fresh 2FA check before disabling 2FA or changing the email address.
  • Consider passkeys as the next step: they're phishing-resistant, while TOTP codes can be phished in real time.
Was this article helpful?

Comments

No comments yet. Have a question or a tip? Be the first to share it.

Leave a comment

Comments are reviewed before they appear.