A complete technical guide to implementing time-based one-time passwords (TOTP/RFC 6238) — how the algorithm works and how to add 2FA to any app.
Two-factor authentication (2FA) is no longer optional for serious applications. TOTP — Time-based One-Time Passwords — is the standard behind every authenticator app (Google Authenticator, Authy, 1Password). This guide explains how it works and how to implement it.
TOTP generates a 6-digit code that changes every 30 seconds. The magic: both the server and the authenticator app compute the same code independently using two shared inputs:
The algorithm:
counter = floor(unix_timestamp / 30)
hmac = HMAC-SHA1(secret, counter_as_8_bytes)
offset = last nibble of hmac
code = (hmac[offset..offset+4] & 0x7FFFFFFF) % 1_000_000
That's the entire TOTP algorithm. The HMAC-SHA1 and bit manipulation is why you can't predict future codes without the secret.
const crypto = require('crypto');
function generateTOTP(base32Secret, window = 0) {
// Decode Base32 secret
const secret = base32Decode(base32Secret);
// Current 30-second window
const counter = Math.floor(Date.now() / 1000 / 30) + window;
// Pack counter as 8-byte big-endian buffer
const buf = Buffer.alloc(8);
buf.writeUInt32BE(Math.floor(counter / 2 ** 32), 0);
buf.writeUInt32BE(counter >>> 0, 4);
// HMAC-SHA1
const hmac = crypto.createHmac('sha1', secret).update(buf).digest();
// Dynamic truncation
const offset = hmac[hmac.length - 1] & 0x0f;
const code = ((hmac.readUInt32BE(offset) & 0x7fffffff) % 1000000)
.toString().padStart(6, '0');
return code;
}
// Validate with ±1 window tolerance for clock skew
function validateTOTP(secret, userCode) {
return [-1, 0, 1].some(w => generateTOTP(secret, w) === userCode);
}
The QR code encodes an otpauth:// URI:
const QRCode = require('qrcode');
function getTOTPQRCodeURL(user, secret, issuer = 'MyApp') {
const otpauth = `otpauth://totp/${issuer}:${user}?secret=${secret}&issuer=${issuer}&algorithm=SHA1&digits=6&period=30`;
return QRCode.toDataURL(otpauth);
}
const crypto = require('crypto');
function generateSecret() {
const bytes = crypto.randomBytes(20); // 160 bits
return base32Encode(bytes); // e.g. 'JBSWY3DPEHPK3PXP'
}
ALTER TABLE users ADD COLUMN totp_secret TEXT;
ALTER TABLE users ADD COLUMN totp_enabled BOOLEAN DEFAULT FALSE;
ALTER TABLE users ADD COLUMN totp_verified_at TIMESTAMPTZ;
-- Prevent TOTP replay attacks
CREATE TABLE totp_used_codes (
user_id UUID REFERENCES users(id),
code TEXT NOT NULL,
used_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (user_id, code)
);
Replay attacks — once a code is used, mark it in the database for the 90-second window (±1 window) to prevent reuse.
Backup codes — generate 8-10 single-use backup codes when the user enables 2FA. Store them hashed (bcrypt).
Rate limiting — limit TOTP verification attempts to prevent brute-forcing. Only 1,000,000 possible codes exist.
Clock skew — allow ±1 window (90 seconds total) for devices with slightly incorrect clocks.
Secret storage — store the TOTP secret encrypted in your database (not just hashed — you need to retrieve it).
Instead of rolling your own, use well-tested libraries:
otplib, speakeasy, @otplib/preset-defaultpyotpgithub.com/pquerna/otpsonata-project/google-authenticatorUse the TOTP Generator tool to test codes with any Base32 secret directly in your browser — useful for debugging your implementation.