Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

Two-Factor Authentication: How Those Six-Digit TOTP Codes Actually Work

About Post

Put your phone in airplane mode. Open your authenticator app. The six-digit codes keep changing every 30 seconds, and they still work when you type them into a website.

No internet. No message from the server. And yet your phone and a server on the other side of the world agree on the same random-looking number, at the same moment.

It feels like magic the first time you notice it. It's actually one of the neatest small ideas in security, and once you understand it, you'll also understand exactly where two-factor authentication is strong and where it isn't.

The trick: a shared secret and a shared clock

The codes come from TOTP, Time-based One-Time Passwords, defined in RFC 6238. The whole scheme needs only two ingredients that both sides have:

  1. A shared secret. A random key the server generates when you enable 2FA. It's inside that QR code you scan, and it's the only time it ever travels between the two sides.
  2. The current time. Your phone and the server both know roughly what time it is.

Each side runs the same calculation, secret + current time → six digits, independently. If the results match, you've proven you have the secret, without sending the secret itself.

Think of two spies who were handed the same codebook before they parted ways. Each day they turn to that day's page. No phone call needed.

Step 1: what's in the QR code

The QR code you scan is just a URI:

otpauth://totp/MyApp:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=MyApp&period=30&digits=6

The secret is Base32 encoded (letters A–Z and digits 2–7), because it was designed to be typed by hand if the camera fails. The other parameters are usually defaults: 30-second periods, 6 digits, SHA-1.

Step 2: turn time into a counter

TOTP doesn't use the exact time. It divides the Unix timestamp by 30 and throws away the remainder:

$counter = intdiv(time(), 30);

Everyone in the same 30-second window gets the same counter. That's why the code changes every 30 seconds, and why it doesn't need perfectly synchronised clocks, just clocks that are roughly right.

Step 3: HMAC, then squeeze it into six digits

The counter and the secret go into HMAC-SHA1, a keyed hash. You can't reverse it to get the secret, and you can't predict the output without the secret. That's the security of the whole scheme.

HMAC gives you 20 bytes. To get six digits, the algorithm uses "dynamic truncation": the last 4 bits of the hash pick an offset, it reads 4 bytes from there, and takes the result modulo 1,000,000. Here's the whole thing in PHP (simplified: $secret is the raw, already Base32-decoded key):

function totp(string $secret, ?int $time = null): string
{
    $counter = intdiv($time ?? time(), 30);
    $hash = hash_hmac('sha1', pack('J', $counter), $secret, true);

    $offset = ord($hash[19]) & 0x0F;
    $number = ((ord($hash[$offset]) & 0x7F) << 24)
        | (ord($hash[$offset + 1]) << 16)
        | (ord($hash[$offset + 2]) << 8)
        | ord($hash[$offset + 3]);

    return str_pad((string) ($number % 1_000_000), 6, '0', STR_PAD_LEFT);
}

pack('J', ...) writes the counter as an 8-byte big-endian number, which is what the spec expects. The & 0x7F drops the sign bit so the result is always positive. That's genuinely the entire algorithm. In a real app you'd use a well-tested library (Laravel Fortify includes 2FA) rather than this function, but it's worth seeing how small it is.

Verifying a code on the server

The verification side is where most of the real-world care goes:

  • Allow a small window. Accept the current counter and one step either side, so a slightly slow phone clock or a slow typist still works.
  • Compare with hash_equals(), not ===, to avoid timing differences.
  • Block replays. Store the last counter that was used successfully and reject the same one again. Otherwise a code someone shoulder-surfed is valid for its whole window.
  • Rate limit attempts. Six digits is a million combinations. Without a limit, that's a brute-force target, not a second factor.
  • Encrypt the secret at rest. Anyone with the database and the secret can generate valid codes forever.

Recovery codes: the part everybody skips

People lose phones. If 2FA has no recovery path, your support team becomes the recovery path, and a support agent who can be talked into disabling 2FA is a weaker link than any algorithm.

The standard answer is a set of single-use recovery codes, shown once when 2FA is enabled. Treat them like passwords: store them hashed, mark each as used, and let users regenerate them. And make the "show them once, save them now" moment clear in the UI.

Why SMS codes are the weakest option

SMS codes look similar to the user, but the security model is different. The code travels over the phone network, so anyone who controls your number gets your code:

  • SIM swapping: an attacker convinces a mobile carrier to move your number to their SIM.
  • Interception: weaknesses in the telecom signalling network can allow messages to be redirected.
  • Lock screen previews: the code pops up on a phone that's lying on a desk.

SMS 2FA is still much better than nothing. But if you're building 2FA, offer an authenticator app first and SMS as a fallback.

The honest limitation: TOTP proves you have the secret. It doesn't prove you're on the real website. A convincing phishing page can ask for your password and your current code, and replay both to the real site within the 30 seconds.

Where passkeys come in

That phishing gap is exactly what passkeys (built on WebAuthn) close. Instead of a code you type, your device holds a private key, and the browser only lets it sign a challenge for the website it was created for. A lookalike domain gets nothing, because there's nothing for the user to type and hand over.

Passkeys are the direction the industry is moving, and they're worth offering if your users' devices support them. But TOTP will be around for years: it's simple, works offline, and every authenticator app speaks it.

If you remember one thing

A TOTP code is just HMAC(secret, time ÷ 30), cut down to six digits. The algorithm is the easy part. The security lives in everything around it: rate limits, replay protection, encrypted secrets and a recovery flow that support can't be tricked into bypassing.

If you've implemented 2FA, which part took you longest: the code itself, the recovery flow, or convincing users to turn it on?

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close