MFA and TOTP codes

Authentication and authorization Mid-level Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Predict the output

What code does the RFC 6238 test vector give at T = 59?

Python 3.13
import hashlib
import hmac
import struct


def totp(secret: bytes, unix_time: int, digits: int = 8) -> str:
    counter = struct.pack(">Q", unix_time // 30)
    mac = hmac.new(secret, counter, hashlib.sha1).digest()
    offset = mac[-1] & 0x0F
    code = struct.unpack(">I", mac[offset:offset + 4])[0] & 0x7FFFFFFF
    return str(code % 10**digits).zfill(digits)


# RFC 6238 test vector: SHA-1 secret, T = 59 s
print(totp(b"12345678901234567890", 59))

Options
Hint 1 of 1

Which time step is T = 59?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A 94287082

Correct. That's the published SHA-1 vector for T = 59 s.

TOTP hashes the 30-second time step with the shared secret and truncates the HMAC to digits. At T = 59 the step is 1, and RFC 6238 lists 94287082 for the 8-digit SHA-1 case, so the code is deterministic.

Why not the others?
  • B 287082. That's the 6-digit form; this call asks for 8 digits.
  • C 00000059. The code is an HMAC, not the time.
  • D A different code each run. Same secret and time step always give the same code.

30-second answer

The server and the app share a secret at enrolment (usually via a QR code). Each code is an HMAC of the secret and the current 30-second time step, truncated to 6 digits (RFC 6238). The server computes the expected code for the current step, usually also one step either side for clock drift, compares in constant time, and records the used step so the same code can't be replayed. Rate-limit attempts, because 6 digits is only a million options.

Practise saying it · 30 s. No recording, just a timer

Explain TOTP.

Say your answer out loud, then check which of these points you covered:

  • Shared secret
  • HMAC of 30 s step
  • ±1 step window
  • No replay, rate-limit
  • Passkeys resist phishing

Deep dive

Store TOTP secrets encrypted; they're as sensitive as passwords.

Offer recovery codes (hashed, single-use) at enrolment.

Passkeys (WebAuthn) resist phishing; TOTP codes can be phished in real time.

SMS codes are weaker (SIM swap) but better than nothing.

TOTP from RFC 6238, checked against its test vector Ran on Python 3.13.5 · mypy --strict
Python 3.13
import hashlib
import hmac
import struct


def totp(secret: bytes, unix_time: int, digits: int = 8) -> str:
    counter = struct.pack(">Q", unix_time // 30)
    mac = hmac.new(secret, counter, hashlib.sha1).digest()
    offset = mac[-1] & 0x0F
    code = struct.unpack(">I", mac[offset:offset + 4])[0] & 0x7FFFFFFF
    return str(code % 10**digits).zfill(digits)


# RFC 6238 test vector: SHA-1 secret, T = 59 s
print(totp(b"12345678901234567890", 59))

Common mistakes

  • Allowing unlimited guesses.
  • Accepting a wide window like ±10 steps.
  • Letting a used code work again within its window.

Senior take & follow-ups

HMAC of secret and time step, ±1 window, replay protection, rate limits; passkeys are stronger.

Follow-up questions

Think of your answer first, then open the hint.

  1. Why can TOTP be phished?

    Show hint

    A fake site can relay the code within 30 seconds.

  2. What's stronger?

    Show hint

    WebAuthn passkeys bound to the real origin.

Suggest a fix if something here is wrong or out of date.