Why Add TOTP to Your Application?
Adding Time-based One-Time Password (TOTP) support to your application is one of the highest-value security improvements you can make. It protects users whose passwords have been compromised and significantly reduces account takeover rates. The good news: integrating TOTP in Python is straightforward thanks to well-maintained libraries.
This guide walks through a complete TOTP implementation using the pyotp library, covering secret generation, QR code provisioning, code verification, and backup codes.
Prerequisites
- Python 3.8 or later
pyotplibrary — install withpip install pyotpqrcodelibrary (optional, for QR provisioning) —pip install qrcode[pil]
Step 1 — Generate a Secret Key per User
Each user needs their own unique shared secret. Generate it server-side when they enable 2FA and store it securely (encrypted at rest in your database).
import pyotp
# Generate a random base32 secret (store this per-user, encrypted)
secret = pyotp.random_base32()
print(secret) # e.g., "JBSWY3DPEHPK3PXP"
Important: Never expose this secret to the client beyond the initial enrollment flow. Treat it like a password hash.
Step 2 — Generate a Provisioning URI for QR Code
Authenticator apps like Google Authenticator and Authy scan a QR code to add a new account. The QR code encodes a provisioning URI in a standard format.
totp = pyotp.TOTP(secret)
# Create the provisioning URI
uri = totp.provisioning_uri(
name="user@example.com", # The user's account identifier
issuer_name="YourAppName" # Your application name
)
print(uri)
# otpauth://totp/YourAppName:user@example.com?secret=JBSWY3...&issuer=YourAppName
Rendering the QR Code
import qrcode
img = qrcode.make(uri)
img.save("totp_qr.png")
# Or serve it as a data URI in an HTML response for in-app display
Display this QR code once during the 2FA enrollment screen. After the user scans and confirms a valid code, delete the QR image and never show the raw secret again.
Step 3 — Verify a User-Submitted Code
When the user logs in and submits their 6-digit code, verify it server-side:
def verify_otp(secret: str, user_code: str) -> bool:
totp = pyotp.TOTP(secret)
# valid_window=1 allows 1 step (30s) before/after for clock drift
return totp.verify(user_code, valid_window=1)
# Usage
is_valid = verify_otp(stored_secret, "482910")
if is_valid:
# Grant access / complete login
pass
else:
# Reject and log the failed attempt
pass
Security note: Always rate-limit OTP verification endpoints. Allow no more than 5–10 attempts before temporarily locking the account or requiring re-authentication.
Step 4 — Prevent Code Reuse
TOTP codes are valid for 30 seconds. If an attacker captures a code in transit, they could reuse it within that window. Prevent this by caching the last-used code per user:
import time
# In your auth service, store used codes with their timestamp
used_codes = {} # In production: use Redis or your session store
def verify_otp_no_reuse(secret: str, user_code: str, user_id: str) -> bool:
totp = pyotp.TOTP(secret)
if not totp.verify(user_code, valid_window=1):
return False
cache_key = f"{user_id}:{user_code}"
current_step = int(time.time()) // 30
if used_codes.get(cache_key) == current_step:
return False # Code already used in this time window
used_codes[cache_key] = current_step
return True
Step 5 — Backup / Recovery Codes
Always provide users with one-time backup codes for account recovery if they lose their device:
import secrets
def generate_backup_codes(count: int = 8) -> list:
"""Generate secure backup codes. Store hashed versions in DB."""
return [secrets.token_hex(5).upper() for _ in range(count)]
# Example output: ["A3F2B1C4D5", "E6F7A8B9C0", ...]
Store only hashed versions of backup codes (using bcrypt or argon2), just like passwords. Mark each as used when redeemed.
Enrollment Flow Summary
- User enables 2FA in account settings.
- Server generates a secret, stores it (pending confirmation).
- Server returns a QR code URI; client renders it.
- User scans with authenticator app and submits a verification code.
- Server verifies the code — if valid, marks 2FA as active.
- Server presents backup codes; user saves them offline.
Further Reading
- pyotp documentation
- RFC 6238 — TOTP standard specification
- RFC 4226 — HOTP standard specification