Spring Boot 2FA: Implement TOTP Authentication
Learn to implement TOTP-based two-factor authentication in Spring Boot from a senior dev.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17+
- ✓Spring Boot 3.x
- ✓Basic familiarity with Spring Security
- ✓Maven or Gradle
- Use Google Authenticator or similar apps with TOTP (RFC 6238).
- Generate a secret per user, store it encrypted, and verify codes server-side.
- Integrate with Spring Security's authentication flow.
- Watch out for clock skew and code reuse attacks.
- Never store secrets in plaintext; use a dedicated secrets vault.
Think of TOTP like a secret handshake that changes every 30 seconds. You and the server agree on a secret number. Every 30 seconds, you both compute a new code based on that secret and the current time. If your code matches, you're allowed in. It's like a constantly changing password that only you and the server know.
Two-factor authentication (2FA) isn't optional anymore—it's expected. If you're building a SaaS platform, fintech app, or even a simple admin dashboard, you need to protect accounts beyond passwords. TOTP (Time-based One-Time Password) is the gold standard: it's open, works with any authenticator app, and doesn't require SMS or email.
But here's the thing: most tutorials show you how to generate a QR code and call it done. They don't tell you about the production nightmares—clock drift, code reuse, backup codes, or how to handle users who lose their phone. I've seen a fintech startup get locked out of their own admin panel because they forgot to handle time synchronization. Don't be that team.
In this article, I'll walk you through implementing TOTP in Spring Boot from scratch. We'll generate secrets, verify codes, and integrate with Spring Security. But more importantly, I'll show you the gotchas that the official docs won't tell you—the stuff you learn after a 2 AM pagerduty call. By the end, you'll have a production-ready 2FA system that doesn't fall apart under real-world conditions.
Why TOTP? The Case for Time-Based One-Time Passwords
TOTP (RFC 6238) is the industry standard for 2FA. Unlike SMS-based codes (which are vulnerable to SIM swapping) or email codes (which are slow and insecure), TOTP generates codes offline on the user's device. The algorithm combines a shared secret with the current time to produce a 6-digit code that changes every 30 seconds.
The beauty of TOTP is that it's stateless: the server doesn't need to track which codes have been used—it just needs the secret and the current time. But this simplicity hides pitfalls: clock skew, code reuse within the same time window, and secret management.
When should you use TOTP? Any time you need strong authentication without relying on external services. It's perfect for internal admin panels, customer-facing apps, and API access. But don't use it as the sole authentication factor—it's a second factor, not a replacement for passwords.
In the next sections, I'll show you how to implement TOTP in Spring Boot using the com.warrenstrange:googleauth library. We'll cover secret generation, QR code provisioning, and code verification. Then we'll integrate it with Spring Security's authentication flow.
googleauth library doesn't handle encryption—that's on you.Generating and Storing TOTP Secrets
The first step is generating a unique secret for each user. The secret is a base32-encoded string that both the server and the authenticator app share. Using GoogleAuthenticator.createCredentials(), we get a key object that contains the secret and a QR code URL.
Once generated, you need to store the secret securely. I recommend encrypting it before persisting. Here's how: use AES-256 encryption with a key derived from the application's master key. Store the encrypted secret in the database along with a flag indicating whether 2FA is enabled.
When the user first sets up 2FA, you present the QR code (or the secret as a string for manual entry). After they scan it, ask them to enter a code to verify everything works. Only after successful verification should you mark 2FA as enabled for that user.
Never store the raw secret in logs or expose it in API responses. If a user needs to re-scan, generate a new secret and invalidate the old one.
VARCHAR(255) column without encryption. A SQL injection later, and all secrets were exposed. Encrypt at rest and in transit.Integrating TOTP with Spring Security Authentication
Now that we have secrets and verification, we need to integrate 2FA into the login flow. The typical approach is to add a second step after password authentication. The user enters their password, then we redirect them to a page where they enter the TOTP code.
In Spring Security, we can implement this by chaining two authentication providers. First, the standard DaoAuthenticationProvider validates the username/password. If successful, we set a flag in the session indicating that the user is "pre-authenticated" but needs 2FA. Then, a custom TotpAuthenticationFilter intercepts the second request and validates the TOTP code.
Alternatively, you can use a single authentication provider that checks both password and TOTP in one step. But that breaks the standard login flow and makes it harder to handle users who haven't set up 2FA.
I prefer the two-step approach because it's cleaner and allows you to redirect users to a 2FA setup page if they haven't configured it yet.
Handling Time Windows and Code Reuse
The GoogleAuthenticator.authorize() method by default allows a window of ±1 interval (30 seconds). That means it accepts codes from the current 30-second window, the previous window, and the next window. This handles clock skew up to 30 seconds.
But what if the server clock is off by more than 30 seconds? You can increase the window by creating a GoogleAuthenticator with a custom TimeBasedOneTimePassword implementation. However, larger windows increase the risk of code reuse. A code from 90 seconds ago is still valid, and if an attacker intercepts it, they could replay it.
To mitigate replay attacks, you should implement a cache of recently used codes. For each user, keep a set of codes that have been used within the current time window. If a code is reused, reject it. This cache can be in-memory (e.g., a Caffeine cache) with a TTL of 90 seconds.
Additionally, consider allowing a configurable window (e.g., 1, 2, or 3 intervals) so you can adjust based on your environment's clock stability.
What the Official Docs Won't Tell You
Here are the hard truths I've learned from production:
- Clock drift is real and will happen. Even with NTP, servers can drift. I've seen a 10-second drift cause intermittent failures. Always monitor
ntpq -pand set up alerts. Consider using a time service like AWS Time Sync for EC2. - Backup codes are not optional. Users will lose their phones. Generate a set of one-time backup codes (e.g., 8 codes) during setup. Store them hashed (BCrypt) in the database. Present them to the user once and tell them to save them. If they use a backup code, invalidate it.
- QR code generation can be a pain. The
GoogleAuthenticatorQRGeneratorcreates a URL that you need to render as a QR. Use a library likezxingto generate the image. But be careful: some users may not have a QR scanner. Always provide the secret as a text fallback. - Testing TOTP is tricky. You can't easily mock time. Use a
Clockabstraction that you can inject. In tests, freeze the clock to a known instant and compute expected codes. - Rate limiting is essential. Don't let attackers brute-force TOTP codes. Implement rate limiting per user (e.g., 5 attempts per minute). Use Spring's
@RateLimiteror a library like Bucket4j. - Session fixation after 2FA. Ensure that after successful 2FA, you invalidate the old session and create a new one. This prevents session fixation attacks.
Testing TOTP Implementation
Testing TOTP is challenging because of time dependence. The key is to abstract the clock. By injecting a Clock instance, you can freeze time in your tests and compute the expected code for a given secret.
Here's a test strategy: 1. Use a fixed instant (e.g., Instant.parse("2023-01-01T00:00:00Z")). 2. Generate a known secret (or use a fixed one for testing). 3. Compute the expected TOTP code using the same algorithm (you can use the library's authorize method with the fixed time). 4. Assert that your verification method returns true for that code.
Also test edge cases: codes from the previous interval, codes from the next interval, and expired codes. Use a mock Clock that advances time to simulate these scenarios.
Don't forget to test replay protection: verify that the same code cannot be used twice.
The 30-Second Lockout: When Clock Drift Killed 2FA
- Always allow a time window of at least ±1 interval (30 seconds) for clock skew.
- Monitor server clock drift with alerts.
- Provide backup codes for users to bypass 2FA in emergencies.
- Log authentication failures with timestamps to detect skew patterns.
- Test your TOTP implementation with a simulated clock drift.
ntpq -p. Ensure NTP is running and drift is < 1 second. Increase verification window to ±2 intervals.timedatectl statusntpq -psudo systemctl restart ntp or sudo chronyc -a makestep.| File | Command / Code | Purpose |
|---|---|---|
| TotpService.java | @Service | Why TOTP? The Case for Time-Based One-Time Passwords |
| TotpSetupController.java | @RestController | Generating and Storing TOTP Secrets |
| SecurityConfig.java | @Configuration | Integrating TOTP with Spring Security Authentication |
| TotpServiceWithReplayProtection.java | @Service | Handling Time Windows and Code Reuse |
| TotpServiceWithClock.java | @Service | What the Official Docs Won't Tell You |
| TotpServiceTest.java | class TotpServiceTest { | Testing TOTP Implementation |
Key takeaways
Interview Questions on This Topic
How does TOTP work under the hood?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Security. Mark it forged?
4 min read · try the examples if you haven't