Guide

How to Implement TOTP 2FA in Your App (Complete Guide)

Adding TOTP-based two-factor authentication to your app is one of the most impactful security improvements you can make. This guide covers everything from generating secret keys to verifying codes and handling edge cases โ€” with a focus on doing it correctly the first time.

How TOTP Works (What You're Building)

TOTP generates 6-digit codes by combining a shared secret key with the current Unix timestamp, divided into 30-second windows. Both your server and the user's authenticator app independently calculate the code for the current window using the same key โ€” if they match, authentication succeeds.

The flow: (1) you generate a secret key and store it for the user, (2) the user scans a QR code to add it to their authenticator app, (3) on every login, you ask for their current code and verify it against the key. Simple in concept; a few important details in practice.

Step 1: Generate a Secret Key

Each user needs a unique, cryptographically random secret key in base32 format. Generate 20 random bytes (160 bits) and base32-encode them โ€” this gives you a 32-character key. Never derive keys from user data or timestamps.

In Python with pyotp: secret = pyotp.random_base32(). In Node.js: const secret = speakeasy.generateSecret({length:20}).base32. Or use our TOTP Secret Key Generator for testing.

Store this key encrypted in your database, associated with the user's account. Treat it with the same sensitivity as a password. Don't log it.

Step 2: Display the Setup QR Code

Users set up TOTP by scanning a QR code with their authenticator app. The QR code encodes an otpauth:// URI in this format: otpauth://totp/YourApp:user@example.com?secret=BASE32SECRET&issuer=YourApp.

Key parameters: secret is the base32 key. issuer is your app name (shown in the authenticator). The label (YourApp:user@example.com) identifies the account. Always include both issuer and a descriptive label โ€” without them, users won't know which entry in their authenticator belongs to your app.

You can preview and test URI generation with our OTPAuth URI Builder. For production: generate the QR server-side using a library like qrcode (Python) or qrcode (npm), or use a trusted client-side library.

Important: After the user has scanned the code, require them to enter their first valid TOTP code before marking 2FA as enabled. This confirms setup succeeded.

Step 3: Verify the Code

On each login, after the user enters their 2FA code, look up their stored secret key and verify the code. Most TOTP libraries accept a window parameter โ€” allow ยฑ1 window (30 seconds either side) to account for clock differences between server and client.

Python: pyotp.TOTP(secret).verify(user_code, valid_window=1). Node: speakeasy.totp.verify({secret, encoding:'base32', token:userCode, window:1}).

Rate limiting is essential. TOTP codes have 10^6 possibilities but only 3 valid codes at any moment (previous, current, next window). Without rate limiting, an attacker can try all possible codes in seconds. Limit to 5 attempts per 15 minutes per account.

Prevent code replay. Once a code has been used successfully, store it (with its timestamp) and reject it if used again within the same window. This prevents an attacker who intercepts a valid code from using it a second time.

Step 4: Store Everything Securely

The TOTP secret key must be stored encrypted at rest. If an attacker gets your database and the keys are unencrypted, they can generate valid codes indefinitely. Encrypt with AES-256-GCM using a key derived from your application secret โ€” not the user's password, since users can change passwords.

Store a flag indicating whether 2FA is enabled for each user, and the timestamp when it was enabled. Also store the last used code (for replay prevention) and the user's recovery/backup codes as bcrypt hashes โ€” never in plain text.

Step 5: Backup Codes

Users will lose their phones. You must provide backup codes โ€” one-time codes they can use to access their account if they can't use their authenticator. Generate 8โ€“10 random codes, each 8โ€“10 characters, when the user enables 2FA. Show them once and instruct the user to save them securely.

Store the backup codes as bcrypt hashes (not plain text). On use, mark the code as used. Once all backup codes are used, prompt the user to generate a new set.

See: 2FA backup codes explained for the user-facing perspective on this.

Common Implementation Mistakes

Skipping clock tolerance: Not allowing ยฑ1 window will cause legitimate failures when user or server clocks are slightly off. Always use a 1-window tolerance.

No rate limiting: A fundamental security hole. Implement it before launch.

No replay protection: A code used once should not be usable again.

Plain text backup codes: If your database leaks, plain text backup codes are immediately usable. Always hash them.

Storing secrets unencrypted: Encrypt TOTP secrets at rest. If your database leaks, unencrypted secrets let attackers generate codes forever.

No confirmation step: Requiring users to enter their first code before enabling 2FA catches setup failures before the user is locked in.

Related Articles

The TOTP Standard: What You Are Implementing

TOTP (RFC 6238) is built on HOTP (RFC 4226). The core algorithm takes two inputs โ€” a shared secret key and the current time step (Unix timestamp divided by 30) โ€” and produces a 6-digit code using HMAC-SHA1 or HMAC-SHA256. Both the client (authenticator app) and server run the same calculation. If the outputs match, authentication succeeds. The shared secret is established once during enrollment via QR code (using the otpauth:// URI format) and stored securely on both sides. The server should support a tolerance window of ยฑ1 time step to accommodate clock drift.

Enrollment Flow

The enrollment flow works as follows: generate a cryptographically random secret key (at least 20 bytes / 160 bits) for the user, encode it as Base32, construct an otpauth:// URI in the format otpauth://totp/Issuer:user@example.com?secret=BASE32SECRET&issuer=Issuer, render this URI as a QR code for the user to scan, prompt the user to enter a valid code to confirm enrollment before saving the secret to your database, and display backup codes at this point. Store the secret in your database encrypted at rest. Never log the secret key in plain text.

Verification Flow

When a user submits a TOTP code during login: retrieve their stored secret key, calculate the expected code for the current time step and the one immediately before and after (to accommodate clock drift), compare the submitted code to all three expected codes using a constant-time comparison function (to prevent timing attacks), reject the code if none match, and if the code matches, record it as used to prevent replay attacks within the same time window. Most TOTP libraries handle the time window tolerance and constant-time comparison automatically.

Libraries and Implementation

Do not implement TOTP from scratch โ€” use a well-tested library. In Python: pyotp is the standard choice. In Node.js: otplib or speakeasy. In PHP: RobThree/TwoFactorAuth. In Go: pquerna/otp. In Ruby: rotp. In Java: GoogleAuthenticator or aerogear-otp-java. These libraries handle secret generation, QR code URI construction, code verification, and time window tolerance. Our TOTP Secret Generator tool can generate test secrets for development purposes.

Frequently Asked Questions

Should I use HMAC-SHA1 or HMAC-SHA256 for TOTP? RFC 6238 defines TOTP with HMAC-SHA1 as the default, and most authenticator apps (including Google Authenticator) only support SHA1. HMAC-SHA256 and SHA512 are defined in the standard but have limited authenticator app support. Use HMAC-SHA1 unless you have a specific reason to use a stronger hash and have confirmed your users' authenticator apps support it.

How do I handle backup codes? Generate 8โ€“10 cryptographically random single-use codes (typically 8โ€“10 alphanumeric characters each) during enrollment, display them once to the user, and store only their bcrypt or Argon2 hashes in your database. When a backup code is used, delete it from the database immediately to prevent reuse.

How do I prevent brute force attacks on TOTP codes? Rate limit TOTP attempts (3โ€“5 attempts per time window is reasonable), implement exponential backoff after failures, and consider temporary account lockout after repeated failures. Track used codes to prevent replay attacks within the valid window.