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.
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:
- Check server clock: run
dateand compare with an NTP source - Verify the secret: ensure the stored secret matches what was enrolled
- Check algorithm parameters: confirm digits, period, and algorithm match the authenticator app
- Log the time offset: if
offset != 0for most verifications, the user's clock is drifting - 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.