Add TOTP 2FA

Add two-factor authentication with TOTP in this secure development lesson. Learn the core concepts, implement a hands-on exercise, troubleshoot common pitfalls, and know what to study next.

Focus: add two-factor authentication with TOTP

Sponsored

You’ve built the login form, hashed the passwords, and maybe even added session management. But every week there’s another story: a phishing kit stole someone’s password, a reused credential from a breach opened the door, and suddenly your “secure” app is the headline. A password alone is no longer a credential — it’s a single point of failure. In this lesson, you’ll learn how to add two-factor authentication with TOTP (Time-based One-Time Passwords), the same mechanism that powers Google Authenticator and Authy, and turn that one weak factor into a two-factor fortress.

The problem this lesson solves

Passwords are the most convenient authentication factor, which is exactly why attackers love them. Phishing, credential stuffing, and password reuse mean that a leaked password is a breached account — no matter how strong your hash is. Single-factor authentication assumes the person typing the password is the legitimate user. That assumption breaks the moment a password lands on a dark-web marketplace.

You need an additional factor that is independent of the password. Something the user has, not something they know. TOTP provides that second factor: a short-lived code generated on the user’s device, verified by your server, and useless to an attacker who only stole the password.

The cost of implementing TOTP is low: a well-tested library, a QR code, and a verification endpoint. The payoff is enormous — according to Microsoft, MFA can block over 99.9% of automated attacks on accounts. This lesson gives you the exact steps to add that protection.

Core concept / mental model

Think of TOTP as a shared secret that slowly dances to the same song. Your server and the user’s authenticator app both start with a secret key (often rendered as a QR code). At any given moment, both sides compute a code from that secret and the current time. If the codes match, the server knows the user possesses the secret — and therefore the physical device.

Here’s the anatomy of TOTP (RFC 6238):

  • Secret: a random base32-encoded string, typically 80–160 bits. This is the “seed” for all future codes.
  • Time step: usually 30 seconds. Both sides agree on this window.
  • HMAC-SHA1: the standard hash function for TOTP (though SHA-256 and SHA-512 are supported by some implementations).
  • Truncation: the HMAC output is converted into a 6–8 digit number by taking a dynamic offset.
  • Validation window: to tolerate clock drift, the server accepts codes from a small window around the current step (e.g., ±1 step).

Think of the secret as the key to a deterministic code generator. Time is the input that changes every 30 seconds. The authenticator app and your server independently produce the same code — this is why the user doesn’t need a network connection to generate a TOTP code.

Pro tip: The secret must be kept encrypted at rest. If an attacker gets both the secret and a password, MFA is just a formality.

How it works step by step

Implementing TOTP in your app follows a predictable flow. Here’s the high-level sequence you’ll encode:

  1. Enrollment: When the user enables 2FA, you generate a random secret and show it as a QR code (otpauth:// URI) that the user scans with their authenticator app.
  2. Verification: The user enters the 6-digit code from their app. Your server recomputes the expected code using the same secret and current time.
  3. Store the secret: Persist the secret (encrypted) on your server, associated with the user’s account.
  4. Login flow: After password verification, prompt for a TOTP code. Only if both are correct does the session start.
  5. Recovery: Provide backup codes or recovery keys so users aren’t locked out if they lose their device.

The cryptographic steps inside TOTP are:

  • Convert the time (time.time() // 30) to a big-endian 8-byte integer.
  • Compute HMAC-SHA1(secret, time_bin).
  • Take the last nibble as an offset, extract 4 bytes, mask the top bit, and reduce modulo 10^6 for 6 digits.

You’ll rarely implement this by hand — use a library like pyotp — but knowing the steps helps you debug and choose the right options.

Hands-on walkthrough

Let’s put this into practice. You’ll build a minimal TOTP enrollment and verification flow using Python and pyotp. First, install the library:

pip install pyotp

Create a file totp_demo.py with the following code. It generates a secret, shows the otpauth URI, simulates the login flow, and verifies a code.

import pyotp
import time

# Step 1: Generate a secret (this would be per-user, stored encrypted)
secret = pyotp.random_base32()
print("Your secret (store encrypted):", secret)

# Step 2: Create a TOTP object with a 30-second time step
# and a valid window of 1 step before/after (allows clock drift)
totp = pyotp.TOTP(secret, interval=30)

# Step 3: Generate the otpauth URI for the QR code
# In a real app, you'd render this as a QR code using pyqrcode or similar.
uri = totp.provisioning_uri(name="alice@example.com", issuer_name="MySecureApp")
print("Scan this URI with your authenticator app:")
print(uri)

# Step 4: Simulate the login flow.
# Normally you'd ask the user for the code, here we generate one.
print("\n--- Simulating login ---")
current_code = totp.now()
print(f"App shows code: {current_code}")

# The user types this code into your login form
user_input = input("Enter the 6-digit code (press Enter to use generated): ").strip()
if not user_input:
    user_input = current_code

# Step 5: Verify with a window of 1 (accepts the previous/next step too)
if totp.verify(user_input, valid_window=1):
    print("✓ Code valid! Logging you in.")
else:
    print("✗ Invalid code. Access denied.")

Run it:

python totp_demo.py

Expected output:

Your secret (store encrypted): JBSWY3DPEHPK3PXP1234
Scan this URI with your authenticator app:
otpauth://totp/MySecureApp:alice@example.com?secret=JBSWY3DPEHPK3PXP1234&issuer=MySecureApp

--- Simulating login ---
App shows code: 123456
Enter the 6-digit code (press Enter to use generated): 
✓ Code valid! Logging you in.

Now, modify the script to save the secret to a file, read it back, and verify a code generated in a separate run. This simulates the persistence required in a real app.

# persist_secret.py
import pyotp
import json

secret = pyotp.random_base32()
# In a real app, encrypt this secret before storing!
with open("user_secret.json", "w") as f:
    json.dump({"secret": secret}, f)
print("Secret saved. Add this to your login flow.")
# verify_with_secret.py
import pyotp
import json

with open("user_secret.json") as f:
    secret = json.load(f)["secret"]

totp = pyotp.TOTP(secret)
user_input = input("Enter code from authenticator app: ")
if totp.verify(user_input, valid_window=1):
    print("Login successful")
else:
    print("Invalid code")

Run both scripts in order. This demonstrates the complete round trip: enrollment → storage → verification.

Pro tip: Always set valid_window to at least 1 to tolerate ~30 seconds of clock drift. Zero tolerance causes false rejects for users with slightly skewed device clocks.

Compare options / when to choose what

You have several ways to add a second factor. Here’s a comparison to help you choose:

Option Strength User friction Offline capability Setup complexity Use case
TOTP (Authenticator app) High (phishing-resistant against basic attacks) Medium (must open app, type code) Yes Low (library, QR code) Most web apps, standard 2FA
SMS SMS-based OTP Low (SIM swap, SS7 attacks) Low (code arrives via text) No (needs cell network) Very low Legacy apps, fallback
Push notification (e.g., Duo) High Low (tap approve) No Medium (needs app infrastructure) Enterprise, high-security apps
Hardware keys (WebAuthn/FIDO2) Very high (phishing-resistant) Low (touch button) Yes Higher (user buys device) Critical admin accounts
Email OTP Low (email can be compromised) Medium No Very low Non-critical apps, recovery

When choose what?

  • Choose TOTP for general consumer apps — it’s the industry-standard, works offline, and doesn’t require a phone number (user privacy). It’s your default.
  • Choose WebAuthn for admin or high-value accounts where phishing resistance is non-negotiable.
  • Avoid SMS entirely unless you have non-technical users and no budget; the security benefit is marginal.
  • Use backup codes alongside TOTP as a recovery fallback.

Troubleshooting & edge cases

"The code doesn't verify even though it looks correct"

  • Clock skew: The most common cause. Your server’s time and the user’s device clock differ by more than 30 seconds. Accept a window (valid_window=1 or 2) or use an NTP-synced server.
  • Wrong secret: The user scanned a different QR code or you regenerated the secret after enrollment. Never regenerate the secret for an existing user without re-enrollment.

"The code worked, then stopped working"

  • Check the interval — if you use a 60-second step while the app uses 30, you’ll see intermittent failures.
  • The user might have multiple TOTP entries for the same account. Encourage removing old entries.

"User lost their device"

  • Provide one-time recovery codes at enrollment. Store them hashed in your database. Without this, you’ll have manual support requests.

Security pitfalls

  • Don’t log the secret or TOTP codes — even in debug mode.
  • Encrypt the secret at rest (e.g., AES-256) and tie it to the user record. If the DB leaks, the secrets must not be plaintext.
  • Rate-limit TOTP attempts to prevent brute force. A 6-digit code has only 1 million combinations, but with rate limiting you can reduce attack surface drastically.

What you learned & what's next

You now know how to add two-factor authentication with TOTP to your application. You can:

  • Explain the core idea: a shared secret plus time generates a one-time password.
  • Generate and provision a secret via QR code or otpauth URI.
  • Verify a user’s TOTP code during login, with a window to handle clock drift.
  • Compare TOTP to other second-factor options and make an informed choice.
  • Troubleshoot common pitfalls like clock skew, secret mismatch, and lost devices.

This is a major boost to your application’s security posture — a password breach is no longer fatal. But MFA is just one layer. Next in the Secure Development path, you’ll likely tackle session management and secure logout, because an authenticated session is only as secure as its lifecycle. That lesson will teach you to handle session fixation, idle timeouts, and secure cookie attributes, closing the loop on a complete authentication flow.

Ready to lock down sessions? Continue to the next lesson.

Practice recap

In your own project, add TOTP 2FA to an existing login form using pyotp. Generate a secret, display a QR code, and verify a code on login. Don’t forget to store the encrypted secret per user and set valid_window=1. Once done, try breaking your own implementation by simulating clock skew or using the wrong secret — then fix it.

Common mistakes

  • Using a valid_window=0 — this rejects codes for users with even minor clock skew. Always set valid_window=1 or more.
  • Storing the TOTP secret in plaintext in the database. If the DB leaks, attacker can generate codes for any user.
  • Regenerating the secret on every login — if the secret changes, previously provisioned codes become invalid and the user must re-enroll.
  • Not providing backup/recovery codes. Users lose devices, and without a fallback you'll lock them out of their accounts.
  • Relying on SMS OTP as the primary second factor. SMS is vulnerable to SIM swapping and SS7 attacks — use TOTP or WebAuthn.

Variations

  1. Use HMAC-SHA256 or HMAC-SHA512 instead of the default HMAC-SHA1 for stronger cryptographic hashing — supported by pyotp.TOTP via the digest parameter.
  2. Implement TOTP yourself using Python's hmac and base64 modules if you want no external dependency — educational but error-prone; prefer a well-tested library.
  3. Combine TOTP with WebAuthn for high-security admin accounts: TOTP as a fallback, WebAuthn as phishing-resistant primary second factor.

Real-world use cases

  • Adding 2FA to a consumer web app's login form, with QR code enrollment and verification via libraries like pyotp.
  • Protecting admin dashboards for DevOps tools where compromised credentials could lead to infrastructure takeover.
  • Securing API access for service accounts by requiring a TOTP code in addition to API keys, especially for sensitive operations.

Key takeaways

  • TOTP adds a second factor based on a shared secret and time, independent of the password.
  • Enrollment involves generating a random secret, showing a QR code, and having the user scan it with an authenticator app.
  • Verification recomputes the expected code using the secret and current time; use a validation window to handle clock drift.
  • Always store the TOTP secret encrypted and never log it or the generated codes.
  • Choose TOTP over SMS for better security and privacy; consider WebAuthn for higher-risk accounts.
  • Provide recovery codes and rate-limit verification attempts to prevent lockouts and brute-force attacks.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.