Think of TOTP (Time-based One-Time Password) as a shared secret that syncs up with a clock, letting your authenticator app and a server agree on a 6-digit code without needing to talk to each other directly.
Let’s see it in action. Imagine you’re setting up a new service, say, "MySecureApp," with two-factor authentication. You open your authenticator app, tap "Add Account," and scan a QR code. That QR code contains two crucial pieces of information: a secret key (a long, random string of characters) and the issuer name ("MySecureApp").
Here’s a peek at what that QR code might actually represent (it’s usually encoded, but this is the data):
otpauth://totp/MySecureApp:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=MySecureApp
The secret is the shared secret, and issuer is just a label for your app.
Once scanned, your authenticator app stores this secret. Now, every 30 seconds, your app performs a calculation. It takes that secret key, combines it with the current time (divided into 30-second intervals), and runs it through a cryptographic hash function (usually HMAC-SHA1). The result of this hash is a long, random-looking string. The app then trims this string down to produce your 6-digit code.
When you log into MySecureApp, you enter your username, password, and the 6-digit code displayed on your authenticator app. MySecureApp’s server has the exact same secret key stored for your account. It independently performs the exact same calculation using its own clock and the shared secret. If the code your app generated matches the code the server calculated, you’re in. The 30-second window means even if someone intercepts a code, it’s only valid for a very short time.
The core problem TOTP solves is enabling strong, stateless authentication without requiring a constant back-and-forth communication channel for every login attempt. The "state" (the shared secret) is distributed, and the "time" acts as the dynamic element. This makes it incredibly efficient for services with millions of users.
The "time" component is critical. It’s usually represented as a large integer counting the number of 30-second intervals since a specific epoch (often January 1, 1970, UTC). Let’s call this the "time step." So, if the current time is T, the time step is floor(T / 30).
The calculation looks something like this conceptually:
- Get Time Step:
time_step = floor(current_unix_timestamp / 30) - Concatenate:
data_to_hash = secret_key + time_step - Hash:
hash_result = HMAC-SHA1(secret_key, time_step)(The actual algorithm uses the time step as the message, not concatenated, but the idea is they’re both inputs). - Truncate: Take a portion of the
hash_resultand convert it into a 6-digit number.
The exact truncation process is defined by RFC 6238. It involves taking specific bytes from the HMAC output and using them to derive the final numeric code. It’s not just picking the first 6 digits; there’s a precise offset and length calculation to ensure consistency.
Most people understand the "shared secret" and "time" parts. What’s less obvious is that the exact same cryptographic hash function (HMAC-SHA1 is common, though SHA256 and SHA512 are also used) and the precise truncation and modulo operations are what make the generated codes identical on both the client and server. A tiny deviation in either the secret, the time sync, or the algorithm implementation on either side will result in mismatched codes.
The next logical step is understanding how to implement TOTP serverside or how to manage secrets securely for applications.