How 2FA codes work: TOTP computed step by step

Compute a six-digit authenticator code by hand from the RFC 6238 test vectors: the time counter, HMAC-SHA-1, dynamic truncation, and why codes fail.

On this page

The six digits in your authenticator app change every 30 seconds, work without a network connection, and still match what the website expects. There is no magic and no server call involved: your phone and the website share one secret and both do the same small calculation on the current time. That calculation is TOTP, defined in RFC 6238 on top of HOTP from RFC 4226. This article computes a code by hand using the official test vectors, so you can see every step, and then lists the reasons a code gets rejected.

Every value below was checked twice: with the code of our TOTP code generator and independently with Node.js's own HMAC.

The ingredients

  • A shared secret. When you scan the setup QR code, your app stores a random key, usually shown as Base32 text like JBSWY3DPEHPK3PXP. The website keeps a copy.
  • The time. Unix time, the number of seconds since 1 January 1970 UTC.
  • Three parameters. The step length (30 seconds), the number of digits (6) and the hash (SHA-1). These are the defaults almost every service uses.

The RFC's test key is the 20 ASCII bytes 12345678901234567890, which in Base32 is GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ. The tool always uses your device clock, so pasting that key shows the code for right now; the past test times below are reproduced in its unit tests.

Step 1: turn the time into a counter

Divide the Unix time by the step and drop the fraction. The RFC's first test time is T = 59 seconds, one second before 00:01:00 UTC on 1 January 1970:

59 ÷ 30 = 1.97, so the counter is 1.

Every second from 30 to 59 gives the same counter, which is why the code stays the same for 30 seconds. The counter is written as 8 bytes, big-endian: 00 00 00 00 00 00 00 01.

Step 2: HMAC the counter with the secret

Compute HMAC-SHA-1 with the secret as the key and those 8 bytes as the message. The result is 20 bytes:

75a48a19d4cbe100644e8ac1397eea747a2d33ab

HMAC is a keyed hash: without the secret you cannot predict it, and seeing old outputs does not reveal the secret.

Step 3: dynamic truncation

Twenty bytes is far too long to type, so RFC 4226 picks four of them in a way that depends on the hash itself:

  1. Take the low 4 bits of the last byte. The last byte is ab, and 0xab & 0x0f = 11.
  2. Read 4 bytes starting at offset 11: c1 39 7e ea.
  3. Clear the top bit (so the number is never negative): 0x41397eea, which is 1,094,287,082 in decimal.

Step 4: keep the last digits

Take that number modulo 10⁶ for a six-digit code: 1,094,287,082 mod 1,000,000 = 287082. With 8 digits it is 94287082, exactly the value printed in RFC 6238's Appendix B for SHA-1 at T = 59.

A code that comes out shorter is padded with leading zeros, which is why a code can start with 0. The tool and every app must keep that zero.

The other test vectors

The same four steps reproduce the rest of the RFC's SHA-1 table:

Unix timeUTC dateCounterOffset8-digit code6-digit code
591970-01-01 00:00:5911194287082287082
11111111092005-03-18 01:58:2937037036407081804081804
12345678902009-02-13 23:31:3041152263389005924005924
20000000002033-05-18 03:33:20666666661569279037279037

The RFC also gives values for SHA-256 and SHA-512 with longer keys, and the tool's tests check all of them. One more row in the RFC uses T = 20,000,000,000, a date in the year 2603. Its counter does not fit in 32 bits, which is the RFC's way of saying an implementation must use a 64-bit counter. The "year 2038 problem" does not apply to a correct TOTP implementation.

Why a code gets rejected

The clock is wrong. Both sides compute the counter from their own clock. If your phone runs 40 seconds slow, it is a whole step behind. RFC 6238 recommends that servers accept at most one step of network delay, and many accept one step either side, so a drift of a few seconds is harmless and a drift of a minute is not. Set your phone's time to automatic.

The code expired while you typed. A code typed at second 29 of its window may arrive at second 31. Waiting for a fresh code is faster than retyping.

The secret was copied wrong. Base32 uses only A to Z and 2 to 7. The digits 0, 1, 8 and 9 never occur, so a secret containing them has been misread (usually O as 0 or I as 1). Our tool names the offending character and its position. It also accepts lower case, spaces and hyphens, because that is how secrets are usually displayed. A length that no encoder can produce, such as 9 characters, means part of the secret was lost.

Non-default parameters. A service that asks for SHA-256, 8 digits or 60-second steps puts them in its setup link. Some authenticator apps ignore those parameters and quietly use the defaults, producing codes that never match. If you control the setup, stick to SHA-1, 6 digits, 30 seconds.

What the secret means for security

Anyone with the secret can generate your codes forever, on any device. That is why the setup QR code is as sensitive as a password: our QR code scanner hides the secret value of an otpauth:// code by default for this reason. Keep the recovery codes the service gives you, and store a backup of the secret somewhere encrypted if your app cannot export it.

TOTP also does not stop real-time phishing. A fake login page can ask for your code and use it within the same 30 seconds. It stops password reuse and leaked passwords from being enough on their own, which is most of what it is for. For the password half, see what makes a strong password.

Common mistakes

  1. Typing a secret by hand and confusing O with 0.
  2. Setting the phone's clock manually, or travelling with automatic time switched off.
  3. Taking a screenshot of the setup QR code "for later" and leaving it in a synced photo library.
  4. Choosing SHA-256 or 8 digits for your own service without checking which apps support it.

To see it live, add the same test secret to your authenticator app and to the TOTP code generator: both should show the same code and the same countdown. If they do not, check the clock first.

Tools mentioned in this article

Back to the blog