PureTools

TOTP: How Two-Factor Authentication Actually Works

PureTools Team· 10 min read
TOTP: How Two-Factor Authentication Actually Works

TOTP: Those 6-Digit Codes Aren't Magic

Every 30 seconds, your authenticator app generates a new 6-digit code. No internet connection needed. No server communication. How does the server know what code you're seeing? Because both sides are running the same algorithm with the same secret.

The Algorithm

TOTP (Time-Based One-Time Password, RFC 6238) works in 4 steps:

  1. Get current time as Unix timestamp
  2. Divide by time step (30 seconds by default) → this gives a counter value
  3. HMAC-SHA1 the counter with the shared secret → produces a 20-byte hash
  4. Truncate the hash to a 6-digit number
// Pseudocode
time_step = floor(unix_timestamp / 30)
hmac = HMAC-SHA1(secret, time_step)
offset = hmac[19] & 0x0f
code = (hmac[offset..offset+3] & 0x7fffffff) % 1000000
// Result: 6-digit code like "482901"

Why It Works Without Internet

During setup, the server generates a random secret and shares it with your authenticator (via QR code). Both sides now have:

  • The same secret key (typically 20 bytes, Base32-encoded)
  • The same algorithm (HMAC-SHA1)
  • The same time step (30 seconds)
  • Synchronized clocks (within reasonable tolerance)

The server doesn't need to contact your phone. It just runs the same calculation with the same secret and checks if the codes match.

The QR Code

When you scan a TOTP QR code, it contains a URI like:

otpauth://totp/MyApp:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=MyApp&algorithm=SHA1&digits=6&period=30
ParameterValueMeaning
secretJBSWY3DPEHPK3PXPBase32-encoded shared secret
issuerMyAppService name shown in authenticator
algorithmSHA1HMAC algorithm (SHA1/SHA256/SHA512)
digits6Code length (6 or 8)
period30Time step in seconds

Implementation in JavaScript

// Node.js TOTP verification
import { createHmac } from 'crypto';

function generateTOTP(secret: Buffer, timeStep = 30): string {
  const counter = Math.floor(Date.now() / 1000 / timeStep);

  // Convert counter to 8-byte big-endian buffer
  const counterBuffer = Buffer.alloc(8);
  counterBuffer.writeBigInt64BE(BigInt(counter));

  // HMAC-SHA1
  const hmac = createHmac('sha1', secret).update(counterBuffer).digest();

  // Dynamic truncation
  const offset = hmac[hmac.length - 1] & 0x0f;
  const code = (
    ((hmac[offset] & 0x7f) << 24) |
    ((hmac[offset + 1] & 0xff) << 16) |
    ((hmac[offset + 2] & 0xff) << 8) |
    (hmac[offset + 3] & 0xff)
  ) % 1000000;

  return code.toString().padStart(6, '0');
}

// Verify with ±1 window for clock drift
function verifyTOTP(secret: Buffer, token: string): boolean {
  for (let i = -1; i <= 1; i++) {
    const adjustedTime = Date.now() + i * 30000;
    // ... generate code at adjusted time
    if (generatedCode === token) return true;
  }
  return false;
}

Python Implementation

import hmac
import hashlib
import struct
import time
import base64

def generate_totp(secret_b32: str, time_step: int = 30) -> str:
    secret = base64.b32decode(secret_b32)
    counter = int(time.time()) // time_step
    counter_bytes = struct.pack('>Q', counter)

    hmac_hash = hmac.new(secret, counter_bytes, hashlib.sha1).digest()

    offset = hmac_hash[-1] & 0x0f
    code = (
        struct.unpack('>I', hmac_hash[offset:offset + 4])[0] & 0x7fffffff
    ) % 1000000

    return f"{code:06d}"

# Example
print(generate_totp('JBSWY3DPEHPK3PXP'))
# "482901" (changes every 30 seconds)

Security Considerations

  • Store secrets encrypted. The TOTP secret in your database is equivalent to a password. Encrypt it at rest.
  • Provide backup codes. Users lose phones. Generate 8-10 single-use backup codes during setup.
  • Allow clock drift. Accept codes from the previous and next time step (±30 seconds).
  • Rate limit verification. Without rate limiting, an attacker has a 1/1,000,000 chance per attempt — brute-forceable at high speed.
  • Don't use SMS. TOTP (authenticator app) is more secure than SMS-based 2FA, which is vulnerable to SIM swapping.

TOTP vs HOTP

FeatureTOTP (Time-Based)HOTP (Counter-Based)
CounterCurrent time / 30Incrementing integer
ExpiryCodes expire after 30sCodes never expire
Sync issuesClock driftCounter desync
SecurityBetter (codes expire)Less secure (replay possible)
Used byGoogle Authenticator, AuthyYubiKey (in HOTP mode), some hardware tokens

Generate TOTP codes: TOTP Generator — enter a secret key and see the live code with countdown timer.