Verify a TOTP Token

Implement server-side TOTP token verification with time window tolerance. Learn the validation algorithm, handle clock drift, prevent replay attacks, and debug rejection issues.

Security

Detailed Explanation

Server-Side TOTP Token Verification

Verifying TOTP tokens on the server is the critical step that actually enforces two-factor authentication. A correct implementation must handle time window tolerance, prevent replay attacks, and provide useful debugging information.

Basic Verification Algorithm

import hmac, hashlib, struct, time

def verify_totp(secret_bytes, token, period=30, digits=6, window=1):
    """Verify a TOTP token with time window tolerance."""
    current_time = int(time.time())

    for offset in range(-window, window + 1):
        counter = (current_time // period) + offset
        # Generate expected token for this time step
        msg = struct.pack('>Q', counter)
        h = hmac.new(secret_bytes, msg, hashlib.sha1).digest()

        # Dynamic truncation
        o = h[-1] & 0x0F
        code = struct.unpack('>I', h[o:o+4])[0] & 0x7FFFFFFF
        expected = str(code % (10 ** digits)).zfill(digits)

        if hmac.compare_digest(expected, token):
            return True, offset  # Valid, with time offset

    return False, None

Time Window Tolerance

The window parameter controls how many adjacent time steps to check:

  • window=0: accept only the current time step (strict, may reject valid codes)
  • window=1: accept T-1, T, T+1 (recommended — covers 90 seconds)
  • window=2: accept T-2 to T+2 (lenient — covers 150 seconds)

Preventing Replay Attacks

A valid TOTP code works for the entire time window. Without replay protection, an attacker who intercepts a code could reuse it within the same window.

Mitigation: track the last successfully used time step per user:

def verify_with_replay_protection(user, token):
    valid, offset = verify_totp(user.secret, token)
    if not valid:
        return False

    current_step = int(time.time()) // 30 + offset
    if current_step <= user.last_used_step:
        return False  # Replay detected

    user.last_used_step = current_step
    return True

Constant-Time Comparison

Always use constant-time string comparison (e.g., hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js) to prevent timing side-channel attacks that could reveal correct digits one by one.

Debugging Rejected Codes

When users report codes being rejected:

  1. Check server clock: run date and compare with an NTP source
  2. Verify the secret: ensure the stored secret matches what was enrolled
  3. Check algorithm parameters: confirm digits, period, and algorithm match the authenticator app
  4. Log the time offset: if offset != 0 for most verifications, the user's clock is drifting
  5. Widen the window temporarily: try window=2 to see if the code matches a wider range

Use Case

Backend developers implementing the TOTP verification endpoint need a robust, secure validation function. This guide is essential when building the login flow's second-factor check, investigating why specific users' codes are being rejected, or conducting a security review of an existing TOTP implementation. The replay protection and constant-time comparison details are often missed in quick implementations but are critical for production security.

Try It — TOTP Generator

Open full tool