🍱 Lunchbox Hands

security

How TOTP 2FA Codes Actually Work: Shared Secrets, Time Steps & QR Codes

A developer-friendly breakdown of TOTP — the six-digit codes from authenticator apps. How the shared secret gets into your phone via a QR code, why the code changes every 30 seconds without any network connection, what HMAC and the time step do, and how the server verifies it.

You scan a QR code, your authenticator app starts spitting out six-digit codes, and somehow your phone and the server agree on the same number every 30 seconds — with no internet connection on the phone. It feels like magic, but TOTP is a small, elegant algorithm you can fully understand in a few minutes. Here’s how it works end to end.

The one secret that makes it all work

TOTP — Time-based One-Time Password — rests on a single idea: your phone and the server share one secret key, established once at setup. After that, both sides independently compute the same code from that secret plus the current time. Nothing is sent over the network to generate a code, which is why your authenticator works in airplane mode.

The whole flow:

  1. The server generates a random secret and shows it to you (as a QR code).
  2. Your app stores the secret.
  3. Forever after, both sides compute code = TOTP(secret, current_time).
  4. At login, you type your code; the server computes its own and checks they match.

What’s really in that QR code

The QR code isn’t an image of the secret — it encodes a special URI, otpauth://, that authenticator apps know how to read:

otpauth://totp/Acme:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Acme&period=30&digits=6&algorithm=SHA1
                    └── label ──┘        └─ base32 secret ─┘  └── parameters ──┘
  • secret — the shared key, Base32-encoded (that’s why it’s all uppercase letters and digits 2–7, no 0, 1, 8, or 9).
  • issuer / label — what shows up in the app (“Acme — alice@…”).
  • period — the time step, almost always 30 seconds.
  • digits — code length, almost always 6.

The QR code is just a convenient way to move that string. You can type the secret in by hand instead — and you can see how any text becomes a scannable code with the QR code generator.

Why the code changes every 30 seconds

This is the “time-based” part. TOTP chops time into fixed windows called time steps:

time_step = floor( current_unix_time / 30 )

Every 30 seconds, time_step ticks up by one, and that number is the only input that changes. The algorithm then computes:

code = HOTP(secret, time_step)

HOTP is an HMAC-based one-time password: it runs HMAC-SHA1 over the secret and the time step, then “truncates” the result down to a 6-digit number. Because both phone and server compute floor(now / 30) from the same clock, they land on the same time_step and therefore the same code — without ever talking to each other.

You can watch the mechanism live in the TOTP generator: paste a secret and the codes roll over exactly on the 30-second boundary, same as your phone.

The clock-skew problem (and the fix)

Two clocks are never perfectly in sync, and you might type the code right as it’s about to expire. If the server only ever accepted the current window, those edge cases would fail constantly.

So servers check a small window of adjacent time steps — typically the current step plus one before and one after (±30 seconds). If your code matches any of those, you’re in. This tolerates minor clock drift and slow typing while keeping the window tight enough that a stolen old code is useless within a minute.

That’s also why TOTP codes are genuinely “one-time”: once a window passes, that code never validates again.

What the server stores — and what it doesn’t

The server stores the shared secret per user, encrypted at rest. Note the asymmetry with passwords: a password can be one-way hashed with bcrypt because the server only needs to verify it. A TOTP secret must be kept in recoverable form because the server has to recompute codes from it. That makes the secret store a high-value target — treat it like the crown jewels.

To stop an attacker who shoulder-surfs one code, good implementations also:

  • Reject reuse within a window (mark a code consumed once accepted).
  • Rate-limit verification attempts — six digits is only a million possibilities, so unlimited guesses would fall fast.

TOTP vs other second factors

MethodNeeds network?Phishing-resistant?Notes
TOTP appNoNoFree, offline, the common default
SMS codesYesNoVulnerable to SIM-swap; avoid if you can
Push approvalYesPartlyConvenient; “approval fatigue” risk
Passkeys / WebAuthnNoYesHardware-bound, the strongest option

TOTP isn’t the strongest factor — it can be phished in real time, since a code typed into a fake site still works for ~30 seconds. Passkeys/WebAuthn beat it on exactly that point. But TOTP is free, works offline, needs no special hardware, and is a massive upgrade over passwords alone — which is why it’s still everywhere.

The recap

  • One shared secret is established at setup via a QR-encoded otpauth:// URI.
  • The code is HMAC-SHA1(secret, floor(now / 30)) truncated to 6 digits — recomputed independently on both sides.
  • It changes every 30 seconds because the time step is the only moving input.
  • The server accepts a small ± window to tolerate clock skew, rejects reuse, and rate-limits guesses.

Understand those four points and the “magic” disappears. Try it hands-on with the TOTP generator, and if you’re building auth around it, read up on how JWTs work for the session tokens you’ll issue after the second factor passes.