Why 2FA Matters
Passwords alone are insufficient. Data breaches expose billions of credentials annually, phishing campaigns trick users into revealing their passwords, and credential stuffing attacks automate login attempts across services. Two-factor authentication (2FA) adds a second layer of verification that renders stolen passwords useless.
2FA Factor Types
| Factor | Examples | Security Level |
|--------|----------|----------------|
| Knowledge | Password, PIN | Weak |
| Possession | Phone, hardware key, authenticator app | Strong |
| Inherence | Fingerprint, face scan | Strong |
| Location | GPS, IP range | Moderate |
| Time | One-time codes | Moderate |
Strong 2FA combines something you know (password) with something you have (phone or key).
TOTP (Time-Based One-Time Password)
TOTP is the most widely implemented 2FA method. The client and server share a secret key, and both derive the same 6-8 digit code from the current time.
Server-Side Implementation
import pyotp
import base64
import os
class TOTPManager:
def __init__(self):
self.issuer = "MyApp"
def generate_secret(self):
"""Generate a new TOTP secret."""
return pyotp.random_base32()
def get_provisioning_uri(self, username, secret):
"""Generate URI for QR code."""
return pyotp.totp.TOTP(secret).provisioning_uri(
name=username,
issuer_name=self.issuer
)
def verify_code(self, secret, code):
"""Verify a TOTP code with a 1-step window for clock drift."""
totp = pyotp.TOTP(secret)
return totp.verify(code, valid_window=1)
Displaying the QR Code
import qrcode
import qrcode.image.svg
def render_qr(uri):
img = qrcode.make(uri, image_factory=qrcode.image.svg.SvgImage)
return img.to_string().decode()
Client-Side Setup
// Generate QR in the browser
const secret = await generateTOTPSecret();
const uri = `otpauth://totp/MyApp:${username}?secret=${secret}&issuer=MyApp`;
// Show QR code
new QRCode(document.getElementById('qrcode'), { text: uri });
// Verify setup
const result = await fetch('/api/2fa/verify', {
method: 'POST',
body: JSON.stringify({ secret, code: userInputCode })
});
SMS-Based 2FA
While less secure than TOTP (vulnerable to SIM swapping), SMS remains widely used due to its simplicity.
import twilio
from twilio.rest import Client
def send_sms_code(phone_number):
code = ''.join(random.choices('0123456789', k=6))
# Store code with expiry in Redis
redis.setex(f"2fa:{phone_number}", 300, code)
client = Client(TWILIO_SID, TWILIO_TOKEN)
client.messages.create(
body=f"Your verification code is: {code}",
from_=TWILIO_PHONE,
to=phone_number
)
return code
Backup Codes
When users lose access to their 2FA device, backup codes provide a recovery path. Generate 8-10 single-use codes:
import hashlib
import secrets
def generate_backup_codes(count=10):
codes = []
hashes = []
for _ in range(count):
code = f"{secrets.randbelow(10**8):08d}"
codes.append(code)
hashes.append(hashlib.sha256(code.encode()).hexdigest())
return codes, hashes
Store only the SHA-256 hashes of backup codes in the database. When a user enters a backup code, hash it and compare against stored hashes, then remove the used hash.
WebAuthn and Passkeys
WebAuthn is the gold standard for 2FA. It uses public-key cryptography: the private key never leaves the user's device, and the server stores only the public key.
Registration
// Server sends challenge
const challenge = await getWebAuthnChallenge();
// Browser creates credential
const credential = await navigator.credentials.create({
publicKey: {
challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
rp: { name: "MyApp", id: "example.com" },
user: {
id: Uint8Array.from(userId, c => c.charCodeAt(0)),
name: username,
displayName: displayName
},
pubKeyCredParams: [{ type: "public-key", alg: -7 }], // ES256
authenticatorSelection: { userVerification: "required" }
}
});
// Send credential to server for storage
await registerCredential(credential);
Authentication
const assertion = await navigator.credentials.get({
publicKey: {
challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
allowCredentials: credentials.map(c => ({
id: Uint8Array.from(c.credentialId, c => c.charCodeAt(0)),
type: 'public-key'
})),
userVerification: 'required'
}
});
await verifyAssertion(assertion);
Rate Limiting 2FA Attempts
2FA endpoints are targets for brute-force attacks. Always rate limit:
| Endpoint | Limit | Window |
|----------|-------|--------|
| TOTP verification | 5 attempts | 15 minutes |
| SMS send | 3 requests | 30 minutes |
| Backup code use | 10 attempts | 1 hour |
| Recovery request | 3 attempts | 24 hours |
User Experience Best Practices
Summary
Implement TOTP as the primary 2FA method, supplement with backup codes for recovery, and offer WebAuthn/passkeys as an upgrade path for security-conscious users. Always rate limit 2FA endpoints, hash backup codes before storage, and provide clear recovery workflows. SMS-based 2FA is better than no 2FA but should be deprecated in favor of app-based or hardware-based authenticators.