How TOTP Actually Works
I implemented TOTP from scratch to understand what happens when you scan that QR code. Here's what I learned.
I always wondered what actually happens when you scan a QR code in Google Authenticator. So I built it from scratch.
The Basic Idea
TOTP is surprisingly simple. You and the server share a secret. You both use that secret plus the current time to generate a 6-digit code. Since you're using the same inputs, you get the same output.
That's it. No network calls from your phone to the server. Just math.
The Tricky Parts
Clock drift was the first thing that bit me. I tested everything locally, it worked perfectly, then tried it on my phone and codes kept failing. My phone's clock was 40 seconds off.
The fix is accepting codes from neighboring time windows. Most implementations accept the previous and next 30-second window too, giving you a 90-second buffer.
Token reuse is something I didn't think about initially. A code is valid for 30 seconds. What stops someone from using it twice? Nothing, unless you track used codes. I added a Redis cache to store recently used tokens and reject duplicates.
The Code
I used the speakeasy library. The core is about 20 lines:
import speakeasy from 'speakeasy';
// Generate a secret when user enables 2FA
const secret = speakeasy.generateSecret({
name: 'MyApp (user@email.com)',
});
// Verify a token they enter
const valid = speakeasy.totp.verify({
secret: secret.base32,
encoding: 'base32',
token: userInput,
window: 1, // Accept ±30 seconds
});The window: 1 handles clock drift. I tried window: 2 initially (150 seconds) but that felt too permissive for a security feature.
What I'd Do Differently
Start with backup codes. I added them as an afterthought, but users will lose access to their authenticator app. It's not an edge case.
Show the manual entry option. Some people can't scan QR codes (corporate phones, accessibility). Always show the Base32 secret as text.
Log failed attempts. When I finally added logging, I saw patterns I wouldn't have noticed otherwise.
The full implementation is in Aegis2FA if you want to see how it all fits together.