Adding TOTP Two-Factor Authentication to Your App
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
- Check email and password as usual.
- If 2FA is on, create a short-lived "pending" session and ask for the code.
- Verify the code (allowing one time-step of clock drift) and only then create the full session.
- 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.