Auth & Security/
Lesson

You open Google Authenticator on your phone. A 6-digit code appears next to your account name. A countdown timer ticks toward zero. When it expires, a new code replaces it. This looks like magic, but the math behind it is straightforward. Both your phone and the server independently compute the same code from two inputs: a shared secret and the current time. No network communication happens during code generation, your phone does not call the server to get the code.

The core algorithm

TOTPWhat is totp?Time-based One-Time Password - a 6-digit code generated from a shared secret and the current time, changing every 30 seconds. Powers authenticator apps. stands for Time-based One-Time Password. It is defined in RFC 6238 and builds on HOTP (HMACWhat is hmac?A keyed hash function that produces a fixed-length digest from a message and a secret key - used in webhook signatures and JWT signing.-based One-Time Password, RFC 4226). The algorithm works in four steps:

  1. Get the current time: take the Unix timestamp (seconds since January 1, 1970)
  2. Divide by the time step: divide the timestamp by 30 (the standard time step) and floor the result. This gives you a counter that changes every 30 seconds.
  3. Compute the HMAC: calculate HMAC-SHA1 using the shared secret as the key and the counter as the message
  4. Truncate to 6 digits: extract a 4-byte section from the HMAC output and convert it to a 6-digit number
// Simplified TOTP generation (conceptual, not production-ready)
function generateTOTP(secret, timeStep = 30) {
  const counter = Math.floor(Date.now() / 1000 / timeStep);
  const hmac = crypto.createHmac('sha1', secret)
    .update(Buffer.from(counter.toString(16).padStart(16, '0'), 'hex'))
    .digest();
  const offset = hmac[hmac.length - 1] & 0x0f;
  const code = (hmac.readUInt32BE(offset) & 0x7fffffff) % 1000000;
  return code.toString().padStart(6, '0');
}

The key insight is that both the server and the authenticator app have the same secret and the same clock. They independently compute the same code. No communication is needed.

Why HMAC-SHA1?

HMAC-SHA1 is a keyed hash function. Given the same key (the shared secret) and the same message (the time counter), it always produces the same output. But given a slightly different input, the output is completely different. An attacker who sees one code cannot predict the next one without knowing the secret.

While SHA-1 has known collision weaknesses, HMAC-SHA1 remains secure for TOTP because the attack surface is different. TOTP does not rely on collision resistance, it relies on the HMAC construction, which is still considered safe. That said, RFC 6238 also supports HMAC-SHA256 and HMAC-SHA512 for applications that want stronger algorithms.

02

The enrollment flow

Before a user can generate TOTPWhat is totp?Time-based One-Time Password - a 6-digit code generated from a shared secret and the current time, changing every 30 seconds. Powers authenticator apps. codes, the server and the authenticator app need to share a secret. This happens once during enrollment:

  1. Server generates a random secret: typically 20 bytes, encoded as base32 (e.g., JBSWY3DPEHPK3PXP)
  2. Server creates a provisioning URI: otpauth://totp/MyApp:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=MyApp&algorithm=SHA1&digits=6&period=30
  3. Server encodes the URI as a QR code: displayed to the user on screen
  4. User scans the QR code: the authenticator app reads the URI and stores the secret locally
  5. User enters a verification code: the server asks for a current TOTP code to confirm the setup worked
  6. Server stores the secret: associated with the user account, encrypted at restWhat is rest?An architectural style for web APIs where URLs represent resources (nouns) and HTTP methods (GET, POST, PUT, DELETE) represent actions on those resources.
Enrollment stepWho does itWhat can go wrong
Generate secretServerWeak random number generator produces predictable secrets
Display QR codeServer to UserQR code cached or logged, secret exposed
Scan QR codeUser to Authenticator appUser screenshots QR code and stores it insecurely
Verify codeUser to ServerServer does not verify, enrollment succeeds without confirmation
Store secretServerSecret stored in plain text in the database

Step 5 is critical and frequently missing. If you skip verification, a user might scan the wrong QR code, mistype the secret, or have a misconfigured authenticator app. They will not discover the problem until they try to log in later and cannot generate a valid code. Always verify during enrollment.

AI pitfall
AI-generated TOTP enrollment almost always skips the verification step. It generates the secret, displays the QR code, and immediately marks 2FA as enabled. This means users can lock themselves out if anything went wrong during setup. Always require the user to enter a valid code before confirming enrollment.
03

The 30-second window and time drift

TOTPWhat is totp?Time-based One-Time Password - a 6-digit code generated from a shared secret and the current time, changing every 30 seconds. Powers authenticator apps. codes change every 30 seconds. But what happens if the server's clock says 10:00:00 and the user's phone says 10:00:02? They compute the same counter (same 30-second window), so the code matches. But what if the phone's clock is off by 45 seconds? They are in different windows, and the code does not match.

The solution is to accept codes from adjacent time windows. Most implementations accept the current window plus one window before and one after, a 90-second total acceptance window:

// TOTP verification with time drift tolerance
function verifyTOTP(secret, userCode, drift = 1) {
  const currentCounter = Math.floor(Date.now() / 1000 / 30);
  for (let i = -drift; i <= drift; i++) {
    const counter = currentCounter + i;
    const expectedCode = generateTOTPFromCounter(secret, counter);
    if (expectedCode === userCode) {
      return { valid: true, drift: i };
    }
  }
  return { valid: false };
}

The drift parameter controls how many adjacent windows to accept. A drift of 1 means three windows (previous, current, next). A drift of 2 means five windows. Larger drift increases tolerance for clock skew but also increases the window of vulnerability for replay attacks.

AI pitfall
AI-generated TOTP verification almost never includes time drift handling. It computes the code for the exact current timestamp and compares. This causes legitimate codes to fail for users whose phone clocks are slightly off. Users report "my code isn't working" and lose trust in the system.
04

Preventing replay attacks

If a user enters code 123456 and it is valid, can an attacker who intercepted that code reuse it within the same 30-second window? Yes, unless you prevent it. The defense is to record the last successfully used counter and reject any counter less than or equal to it:

// Replay prevention: track last used counter
async function verifyWithReplayProtection(userId, secret, userCode) {
  const lastCounter = await db.getLastUsedCounter(userId);
  const currentCounter = Math.floor(Date.now() / 1000 / 30);

  for (let i = -1; i <= 1; i++) {
    const counter = currentCounter + i;
    if (counter <= lastCounter) continue; // Reject replayed codes
    const expectedCode = generateTOTPFromCounter(secret, counter);
    if (expectedCode === userCode) {
      await db.setLastUsedCounter(userId, counter);
      return true;
    }
  }
  return false;
}
05

Popular authenticator apps

AppPlatformKey featuresBackup and sync
Google AuthenticatoriOS, AndroidSimple, widely recognizedCloud backup via Google account
AuthyiOS, Android, DesktopMulti-device sync, encrypted backupsCloud backup via Authy account
1PasswordAll platformsIntegrated with password managerSynced across all 1Password devices
Microsoft AuthenticatoriOS, AndroidPush notifications for Microsoft accountsCloud backup via Microsoft account
Bitwarden AuthenticatorAll platformsOpen source, integrated with vaultSynced with Bitwarden vault

The choice of authenticator app is the user's decision, not yours. Your server implements the standard TOTPWhat is totp?Time-based One-Time Password - a 6-digit code generated from a shared secret and the current time, changing every 30 seconds. Powers authenticator apps. protocolWhat is protocol?An agreed-upon set of rules for how two systems communicate, defining the format of messages and the expected sequence of exchanges., and any compliant app works. Do not tie your implementation to a specific app.

06

Storing TOTPWhat is totp?Time-based One-Time Password - a 6-digit code generated from a shared secret and the current time, changing every 30 seconds. Powers authenticator apps. secrets securely

The TOTP shared secret is equivalent to a password, anyone who has it can generate valid codes. Storing it in plain text in your database means a database breach compromises every user's 2FAWhat is 2fa?Two-factor authentication - requiring a second verification step (like a phone code) on top of your password to prove it's really you..

Encrypt TOTP secrets at restWhat is rest?An architectural style for web APIs where URLs represent resources (nouns) and HTTP methods (GET, POST, PUT, DELETE) represent actions on those resources. using an application-level encryptionWhat is encryption?Scrambling data so only someone with the right key can read it, protecting information from being intercepted or stolen. key stored outside the database (in a secret manager like AWS Secrets Manager or HashiCorp Vault). This way, a database dump alone does not reveal the secrets.

// Encrypt before storing
const encryptedSecret = encrypt(totpSecret, process.env.TOTP_ENCRYPTION_KEY);
await db.saveTotpSecret(userId, encryptedSecret);

// Decrypt when verifying
const encryptedSecret = await db.getTotpSecret(userId);
const totpSecret = decrypt(encryptedSecret, process.env.TOTP_ENCRYPTION_KEY);
const isValid = verifyTOTP(totpSecret, userCode);
AI pitfall
AI-generated 2FA implementations store the TOTP secret in plain text in the database, right next to the password hash. If the database is breached, the attacker has both the password hash (which they can try to crack) and the TOTP secret (which they can use immediately). Always encrypt TOTP secrets with a key that lives outside the database.