What is Two-Factor Authentication?
Two-factor authentication (2FA) requires users to verify their identity with a second factor beyond their password: something they have (TOTP app, hardware key) or something they receive (SMS code, email OTP). 2FA dramatically reduces account takeovers because stolen passwords alone are insufficient. TOTP (Time-based One-Time Password, RFC 6238) is the gold standard — used by Google Authenticator, Authy, and 1Password. SMS is convenient but phishable. WebAuthn/FIDO2 hardware keys are the most secure.
Requirements
- Support TOTP (authenticator apps) and SMS-based OTP
- Enrollment: generate a secret, display QR code, verify first code before enabling
- Verification: on login, after password, prompt for 2FA code; verify within 30-second window
- Recovery codes: generate 10 one-time backup codes at enrollment
- Rate limiting: max 5 attempts per 10 minutes before lockout
- Audit log every 2FA event (enabled, disabled, used, failed)
TOTP Protocol
TOTP is derived from HOTP (HMAC-based OTP): T = floor(current_unix_time / 30) # 30-second window TOTP = HOTP(secret, T) HOTP = HMAC-SHA1(secret, T) → truncate to 6 digits The secret is a random 20-byte value, base32-encoded for QR code display. Both the server and the authenticator app compute TOTP independently using the shared secret and the current time — no network call needed.
Data Model
UserTwoFactor(
user_id UUID PRIMARY KEY,
method ENUM(TOTP, SMS, DISABLED),
totp_secret VARCHAR, -- base32-encoded, encrypted at rest
phone VARCHAR, -- for SMS method
enabled BOOL DEFAULT false,
enrolled_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ
)
TwoFactorRecoveryCode(
code_id UUID PRIMARY KEY,
user_id UUID NOT NULL,
code_hash VARCHAR NOT NULL, -- bcrypt hash of recovery code
used BOOL DEFAULT false,
used_at TIMESTAMPTZ
)
TwoFactorAttempt(
attempt_id UUID PRIMARY KEY,
user_id UUID,
success BOOL,
method VARCHAR,
ip_address VARCHAR,
created_at TIMESTAMPTZ
)
Enrollment Flow
def enroll_totp(user_id):
# 1. Generate secret
secret = base64.b32encode(os.urandom(20)).decode()
# 2. Store (not yet enabled)
db.upsert(UserTwoFactor(user_id=user_id, method='TOTP',
totp_secret=encrypt(secret), enabled=False))
# 3. Generate QR code URI
uri = f'otpauth://totp/MyApp:{user_email}?secret={secret}&issuer=MyApp'
qr_png = qrcode.make(uri) # display to user
# 4. Return QR + secret (user scans with authenticator app)
return {'qr': qr_png, 'secret': secret, 'uri': uri}
def confirm_enrollment(user_id, code):
user_2fa = db.get(UserTwoFactor, user_id)
secret = decrypt(user_2fa.totp_secret)
if verify_totp(secret, code):
db.update(UserTwoFactor, user_id, enabled=True, enrolled_at=now())
# Generate recovery codes
codes = [secrets.token_hex(8) for _ in range(10)]
for code in codes:
db.insert(TwoFactorRecoveryCode(user_id=user_id,
code_hash=bcrypt.hash(code)))
return {'recovery_codes': codes} # show once, never again
raise InvalidCode()
Verification with Time Window
import hmac, hashlib, struct, time
def verify_totp(secret, code, window=1):
# window=1 allows T-1, T, T+1 (compensates for clock skew)
secret_bytes = base64.b32decode(secret)
T = int(time.time() // 30)
for delta in range(-window, window + 1):
expected = generate_totp(secret_bytes, T + delta)
if hmac.compare_digest(str(expected).zfill(6), str(code).zfill(6)):
return True
return False
def generate_totp(secret_bytes, T):
msg = struct.pack('>Q', T)
h = hmac.new(secret_bytes, msg, hashlib.sha1).digest()
offset = h[-1] & 0x0f
code = struct.unpack('>I', h[offset:offset+4])[0] & 0x7fffffff
return code % 1000000
Rate Limiting and Lockout
def check_rate_limit(user_id, ip):
key = f'2fa_attempts:{user_id}'
attempts = redis.incr(key)
if attempts == 1:
redis.expire(key, 600) # 10-minute window
if attempts > 5:
raise TooManyAttempts(retry_after=redis.ttl(key))
Key Design Decisions
- TOTP window ±1 — compensates for clock drift between user device and server (up to 30 seconds)
- Encrypt TOTP secret at rest — it’s a long-lived credential; treat it like a password
- bcrypt for recovery codes — one-way hash; if DB is compromised, codes can’t be extracted
- Disabled by default, require confirmation code before enabling — prevents enrolling with wrong secret
- Rate limit per user + IP — user-level prevents enumeration; IP-level prevents distributed attacks
Two-factor authentication and account security design is discussed in Google system design interview questions.
Two-factor authentication and security system design is covered in Coinbase system design interview preparation.
Authentication and account security system design is discussed in Stripe system design interview guide.
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering