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:[email protected]?secret=BASE32SECRET&issuer=YourApp.

Key parameters: secret is the base32 key. issuer is your app name (shown in the authenticator). The label (YourApp:[email protected]) 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