How TOTP two-factor codes actually work (and why 30 seconds matters)

6 min read
security
2fa
totp
authentication

Scan a QR code with an authenticator app and from then on your phone shows a six-digit number that changes every 30 seconds. The server you log in to never talks to that app, yet it knows the same number at the same moment. No push notification, no internet connection on the phone, nothing to intercept in transit. That is TOTP — Time-based One-Time Password — and the trick is that both sides share a secret and agree on the time.

The moving parts

TOTP is built on three things:

  • A shared secret — a random byte string, usually 20 bytes, shown as a Base32 string like JBSWY3DPEHPK3PXP. Both the server and the authenticator store it. It is set up once.
  • The current Unix time, in seconds.
  • HMAC-SHA1 — the same keyed hash used to sign requests and JWTs. One side hashes the time with the secret, the other side checks it.

There is no second channel. The code is derived from data both sides already hold.

The 30-second window

The time is not used directly. It is divided into a 30-second step:

counter = floor(unix_time / 30)

A 30-second window is long enough for a human to read and type six digits, short enough that a captured code is stale within a minute. It also forgives small clock drift — your phone and the server rarely agree to the second, but both land in the same window.

Shorten the window and typing becomes a race; lengthen it and a stolen code stays useful for too long. RFC 6238 settled on 30 seconds, and a decade of muscle memory followed.

From a hash to six digits

HMAC-SHA1 produces 20 bytes. You cannot type 20 bytes, so TOTP trims them to a six-digit number using dynamic truncation:

offset      = last 4 bits of the hash   (0–15)
four_bytes  = hash[offset .. offset+4]
number      = four_bytes & 0x7FFFFFFF   (strip the sign bit)
code        = number mod 1_000_000       →  6 digits, zero-padded

Different windows hash to different bytes, so the truncated number jumps around with no pattern. The leading zero matters — 019384 is a valid code.

What an attacker needs

A password alone is guessable. A TOTP code is not, because guessing it means knowing the shared secret, and that secret is never sent over the wire after enrolment. The realistic attacks are different:

  • Secret theft — if the secret leaks from the server's database, every account on that service is compromisable until keys are rotated. Store them encrypted, not in plain text.
  • Phishing in real time — a fake login page that forwards your code to the attacker within the 30-second window. TOTP cannot fix this; passkeys and number-matching prompts can.
  • Recovery bypass — if "backup codes" or SMS fallback are weaker than TOTP itself, that becomes the path of least resistance.

TOTP is a meaningful upgrade over a password alone and a step down from hardware-backed passkeys. For most personal accounts it is the right middle ground.

Generating a code without an app

The TOTP Generator runs the whole RFC 6238 flow in your browser with the Web Crypto API: paste a Base32 secret (or the otpauth:// URI from a QR code) and it shows the live six-digit code with a countdown ring. The secret never leaves your tab — useful for testing your own login flow, recovering access when you are between phones, or just understanding what the authenticator app is doing.

Setting it up safely

  1. Generate a strong, unique password first — 2FA is not a substitute for a bad password.
  2. Write down the recovery codes somewhere offline. Losing your phone should not lock you out.
  3. If you build TOTP into your own app, store the secret encrypted and give users backup codes from a CSPRNG, not from a predictable counter.

The maths is small; the security comes from the shared secret staying secret and the window being short.

Tools from this article

Comments

No login needed. Comments appear after a quick review.

Protected by an on-site captcha — no third-party trackers.

Loading comments…

← All articles