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
  • pyotp library — install with pip install pyotp
  • qrcode library (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

  1. User enables 2FA in account settings.
  2. Server generates a secret, stores it (pending confirmation).
  3. Server returns a QR code URI; client renders it.
  4. User scans with authenticator app and submits a verification code.
  5. Server verifies the code — if valid, marks 2FA as active.
  6. Server presents backup codes; user saves them offline.

Further Reading

  • pyotp documentation
  • RFC 6238 — TOTP standard specification
  • RFC 4226 — HOTP standard specification