Implementing TOTP-Based MFA
Password breaches are routine. Credential stuffing works because users reuse passwords. MFA adds a second factor that attackers cannot obtain from a database dump — a time-limited code generated by something the user possesses (their phone) or knows (a backup code).
TOTP (Time-based One-Time Password, RFC 6238) is the most widely deployed software MFA method. It powers Google Authenticator, Authy, 1Password, and every major platform's 2FA. Implementation is straightforward, but the details around secret storage, clock drift, enrollment UX, and recovery determine whether your MFA actually protects users or just annoys them.
How TOTP works
Both server and authenticator app share a secret key. The code is:
TOTP = HOTP(secret, floor(current_unix_time / 30))
HOTP = truncate(HMAC-SHA1(secret, counter))
The code is six digits, valid for 30 seconds. Server and app independently compute the same code from the same secret and time.
Enrollment flow
Generate a secret, show a QR code, verify the user can produce valid codes, then activate:
import pyotp
import qrcode
import io, base64
from cryptography.fernet import Fernet
def enroll_totp(user_id: str) -> dict:
secret = pyotp.random_base32()
totp = pyotp.TOTP(secret)
provisioning_uri = totp.provisioning_uri(
name=user.email,
issuer_name="MyApp",
)
# Generate QR code
qr = qrcode.make(provisioning_uri)
buffer = io.BytesIO()
qr.save(buffer, format="PNG")
qr_base64 = base64.b64encode(buffer.getvalue()).decode()
# Store encrypted secret (not yet activated)
encrypted_secret = encrypt_secret(secret)
db.store_pending_totp(user_id, encrypted_secret)
return {
"qr_code": qr_base64,
"secret": secret, # show for manual entry
"provisioning_uri": provisioning_uri,
}
The user scans the QR code or manually enters the secret. Then verify they configured it correctly:
def verify_enrollment(user_id: str, code: str) -> bool:
encrypted_secret = db.get_pending_totp(user_id)
secret = decrypt_secret(encrypted_secret)
totp = pyotp.TOTP(secret)
if totp.verify(code, valid_window=1):
db.activate_totp(user_id)
backup_codes = generate_backup_codes(user_id, count=10)
return {"success": True, "backup_codes": backup_codes}
return {"success": False}
Only activate MFA after successful verification. Generate backup codes at activation — this is the user's only recovery path if they lose their device.
Verification during login
After password authentication succeeds, require TOTP:
def verify_totp_login(user_id: str, code: str) -> bool:
encrypted_secret = db.get_active_totp(user_id)
if not encrypted_secret:
return True # MFA not enabled, skip
secret = decrypt_secret(encrypted_secret)
totp = pyotp.TOTP(secret)
if totp.verify(code, valid_window=1):
return True
# Check backup codes as fallback
if verify_backup_code(user_id, code):
return True
return False
Track failed attempts and rate-limit — lock out after 5 consecutive failures for 15 minutes.
Secret storage
Never store TOTP secrets in plaintext:
from cryptography.fernet import Fernet
class SecretVault:
def __init__(self, kms_key_id: str):
self.fernet = Fernet(self._load_data_key(kms_key_id))
def encrypt(self, secret: str) -> bytes:
return self.fernet.encrypt(secret.encode())
def decrypt(self, encrypted: bytes) -> str:
return self.fernet.decrypt(encrypted).decode()
Use a dedicated encryption key from AWS KMS, GCP Cloud KMS, or HashiCorp Vault — separate from database-level encryption.
Backup codes
One-time recovery codes for device loss:
import secrets
import hashlib
def generate_backup_codes(user_id: str, count: int = 10) -> list[str]:
codes = [secrets.token_hex(4).upper() for _ in range(count)] # 8-char codes
hashed = [hashlib.sha256(c.encode()).hexdigest() for c in codes]
db.store_backup_codes(user_id, hashed)
return codes # show once to user, never stored in plaintext
def verify_backup_code(user_id: str, code: str) -> bool:
code_hash = hashlib.sha256(code.encode()).hexdigest()
if db.consume_backup_code(user_id, code_hash):
notify_user(user_id, "A backup code was used. If this wasn't you, reset your MFA.")
return True
return False
Each backup code works once. Hash them like passwords. Notify the user when one is consumed.
Recovery flows
Users will lose their phone. Plan for it:
- Backup codes — primary recovery. User enters a backup code instead of TOTP.
- Admin reset — support team verifies identity (video call, ID check) and resets MFA.
- Recovery email — send a time-limited link to disable MFA (requires access to registered email + password).
- Recovery key — shown once at enrollment, stored by the user offline.
Never leave a user permanently locked out. Every MFA implementation needs a recovery path that does not bypass security entirely.
Security considerations
- Rate limit verification attempts — 5 failures per 15 minutes per user.
- Do not leak MFA status — login should not reveal whether MFA is enabled before password verification.
- Secure the enrollment endpoint — requires authenticated session.
- Audit log all MFA events — enrollment, verification, failures, backup code use, resets.
- Allow ±1 time window only — wider windows increase brute-force surface.
# Brute force math: 6-digit code = 1,000,000 possibilities
# At ±1 window = 3 valid codes at any time
# At 5 attempts per 15 min = 480 attempts/day
# Expected time to brute force: ~2,083 days (acceptable)
# At ±5 window = 11 valid codes → do NOT do this
Resources
- RFC 6238: TOTP specification
- RFC 4226: HOTP specification
- pyotp Python library
- OWASP Authentication Cheat Sheet
- Google Authenticator key URI format
Production notes for LLM stacks
When mfa-totp-implementation sits on an inference or RAG path, treat user prompts and retrieved chunks as untrusted input. Log correlation IDs and policy decisions—not raw prompts—in production telemetry. Gate risky operations behind explicit authorization at the gateway, not inside ad-hoc tool handlers.
Roll out changes with shadow mode first: record what would have happened under the new rule without blocking traffic. Compare deny rates, latency impact, and false positives for at least one business week before enforcing. Pair enforcement with a runbook entry: symptom, dashboard, rollback (feature flag or config), and owner.
Load-test with production-shaped concurrency. LLM workloads burst differently from CRUD APIs—tail latency and token throttling dominate. If implementing totp-based mfa protects an invariant (security, billing, data residency), prove the invariant with an automated test that fails CI when someone removes the check.
What teams get wrong
Teams copy a reference architecture without matching their compliance tier, then discover in audit that logs, backups, or support exports reintroduced the data they thought they had eliminated. Another pattern: shipping the demo integration without idempotency, then fighting duplicate side effects when clients retry on model timeouts.
Document the tradeoff you chose—strictness vs recall, cost vs quality, sync vs async—and the metric that tells you if the choice still holds six months later.
Frequently asked questions
What is the difference between TOTP and HOTP?
TOTP (Time-based OTP) generates codes from the current time and a shared secret — codes change every 30 seconds. HOTP (HMAC-based OTP) generates codes from an incrementing counter. TOTP is what Google Authenticator, Authy, and 1Password use. HOTP is common in hardware tokens. TOTP is preferred for software authenticators because it self-synchronizes via time.
How many time steps should I allow for clock drift?
Allow ±1 time step (30 seconds) by default — this covers most clock drift between server and phone. For environments with known time sync issues, allow ±2 steps. Never allow more than ±2 or brute-force becomes feasible (5 valid codes at any moment with ±2).
How should I store TOTP secrets?
Encrypt TOTP secrets at rest with AES-256 using a key from a KMS or HSM. Never store secrets in plaintext. The encryption key should be separate from the database encryption key so a database breach alone does not expose MFA secrets.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →