Skip to content

Multi-Factor Authentication with TOTP — Time-Based One-Time Passwords Deep Dive

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Multi. We cover key concepts, practical examples, and best practices to help you master this topic.

TOTP (Time-based One-Time Password) generates a numeric code from a shared secret and the current Unix time, providing a second authentication factor that works offline and does not require network connectivity.

What You'll Learn

TOTP algorithm internals, secret provisioning via provisioning URIs and QR codes, time-step configuration, verification window tolerance, and backup Code Generation.

Why It Matters

TOTP is the most widely deployed MFA method. It works offline, does not require SMS infrastructure, and is supported by all authenticator apps (Google Authenticator, Authy, Microsoft Authenticator).

Real-World Use

GitHub, Google, and AWS all use TOTP for MFA. Durga Antivirus Pro uses TOTP for partner portal authentication, generating secrets via QR codes and verifying codes on each sensitive operation.

sequenceDiagram
    participant User as User
    participant App as Authenticator App
    participant Server as Auth Server

    Server->>Server: Generate random secret (160 bits)
    Server->>App: Provisioning URI
otpauth://totp/Durga:user?secret=...&issuer=Durga App->>App: Store secret in secure storage Note over App: TOTP = HMAC-SHA1(secret, time_counter)[truncated] User->>App: Read 6-digit code User->>Server: Login + TOTP code Server->>Server: Compute expected code
(current time ± 30s drift) Server->>User: Access granted or denied

Code Example: TOTP Generation and Verification

import hmac, hashlib, struct, time, base64
from flask import Flask, request, jsonify

app = Flask(__name__)

def generate_totp_secret():
    """Generate a random base32-encoded secret for TOTP."""
    import secrets
    random_bytes = secrets.token_bytes(20)  # 160 bits
    return base64.b32encode(random_bytes).decode()

def totp(secret, time_step=30, digits=6):
    """Generate a TOTP code using HMAC-SHA1."""
    # Get current time counter
    counter = int(time.time() / time_step)

    # Pack counter as 8-byte big-endian
    counter_bytes = struct.pack(">Q", counter)

    # Decode base32 secret
    key = base64.b32decode(secret)

    # Compute HMAC-SHA1
    h = hmac.new(key, counter_bytes, hashlib.sha1).digest()

    # Dynamic truncation
    offset = h[-1] & 0x0F
    truncated = struct.unpack(">I", h[offset:offset+4])[0] & 0x7FFFFFFF

    # Return last 6 digits
    return f"{truncated % 10**digits:0{digits}d}"

def verify_totp(secret, code, window=1, digits=6):
    """Verify a TOTP code with time drift window."""
    current = int(time.time() / 30)

    for i in range(-window, window + 1):
        counter = current + i
        counter_bytes = struct.pack(">Q", counter)
        key = base64.b32decode(secret)

        h = hmac.new(key, counter_bytes, hashlib.sha1).digest()
        offset = h[-1] & 0x0F
        truncated = struct.unpack(">I", h[offset:offset+4])[0] & 0x7FFFFFFF
        expected = f"{truncated % 10**digits:0{digits}d}"

        if hmac.compare_digest(code, expected):
            return True

    return False

Code Example: TOTP Provisioning API

import qrcode
import io
import base64 as b64

# User secrets store
totp_secrets = {}

@app.route("/api/mfa/totp/setup", methods=["POST"])
def setup_totp():
    """Generate TOTP secret and provisioning URI."""
    user_id = request.json.get("user_id")

    secret = generate_totp_secret()
    totp_secrets[user_id] = secret

    # Generate provisioning URI (standard format for authenticator apps)
    provisioning_uri = (
        f"otpauth://totp/Durga:{user_id}?"
        f"secret={secret}&issuer=Durga&algorithm=SHA1&digits=6&period=30"
    )

    # Generate QR code as base64 PNG
    qr = qrcode.QRCode(box_size=8, border=2)
    qr.add_data(provisioning_uri)
    qr.make(fit=True)
    img = qr.make_image()
    buffer = io.BytesIO()
    img.save(buffer, format="PNG")
    qr_b64 = b64.b64encode(buffer.getvalue()).decode()

    return jsonify({
        "secret": secret,
        "provisioning_uri": provisioning_uri,
        "qr_code_base64": qr_b64,
        "backup_codes": generate_backup_codes(user_id),
        "message": "Scan QR code with authenticator app"
    })

@app.route("/api/mfa/totp/verify", methods=["POST"])
def verify_totp_code():
    """Verify a TOTP code."""
    user_id = request.json.get("user_id")
    code = request.json.get("code", "")
    secret = totp_secrets.get(user_id)

    if not secret:
        return jsonify({"error": "TOTP not configured"}), 400

    if verify_totp(secret, code):
        return jsonify({"verified": True})
    else:
        return jsonify({"verified": False}), 401

Code Example: Backup Code Generation and Validation

import secrets, hashlib

# Backup codes per user
user_backup_codes = {}

def generate_backup_codes(user_id, count=8):
    """Generate one-time backup codes."""
    codes = []
    hashed_codes = []

    for _ in range(count):
        code = f"{secrets.randbelow(10**8):08d}"
        codes.append(code)
        hashed_codes.append(hashlib.sha256(code.encode()).hexdigest())

    user_backup_codes[user_id] = {
        "hashes": hashed_codes,
        "used": set()
    }

    return codes  # Return plain codes once — user must save them

@app.route("/api/mfa/totp/verify", methods=["POST"])
def verify_mfa():
    """Verify TOTP code or backup code."""
    user_id = request.json.get("user_id")
    code = request.json.get("code", "")

    # Try TOTP first
    secret = totp_secrets.get(user_id)
    if secret and verify_totp(secret, code):
        return jsonify({"verified": True, "method": "totp"})

    # Try backup code
    codes = user_backup_codes.get(user_id)
    if codes:
        code_hash = hashlib.sha256(code.encode()).hexdigest()
        if code_hash in codes["hashes"] and code_hash not in codes["used"]:
            codes["used"].add(code_hash)
            return jsonify({
                "verified": True,
                "method": "backup",
                "remaining_codes": len(codes["hashes"]) - len(codes["used"])
            })

    return jsonify({"verified": False}), 401

Common Mistakes

1. Zero Time Window

With zero tolerance for clock drift, users whose phones are a few seconds off will fail to authenticate. Use a window of +/- 1 time step (30 seconds).

2. Using SHA256/SHA512 Instead of SHA1

While SHA1 is cryptographically weaker than SHA256, TOTP uses HMAC-SHA1 which remains secure for this purpose. Most authenticator apps only support SHA1. Use SHA1 for compatibility.

3. Not Invalidating Used Backup Codes

Backup codes are single-use. After verification, mark them as used. Replay attacks using the same backup code must be prevented.

4. Revealing the Secret After Setup

The TOTP secret should only be shown once during setup. If the user needs to reconfigure, generate a new secret and invalidate the old one.

5. No Rate Limiting on Verification

Attackers can brute-force 6-digit TOTP codes (1M combinations). Rate limit to 5 attempts per minute per user. With a window of +/- 1, the attacker would need 3+ attempts per code position.

Practice Questions

  1. How does the TOTP algorithm generate time-based codes?
  2. What is the purpose of the provisioning URI?
  3. Why is a verification window needed?
  4. How do backup codes provide fallback access?
  5. Why should TOTP verification be rate-limited?

Answers:

  1. TOTP uses HMAC-SHA1 with a secret key and the current time counter (Unix time / 30). The HMAC output is dynamically truncated to produce a 6-digit code.
  2. The provisioning URI encodes the secret, issuer, user, and algorithm in a standard format (otpauth://) that authenticator apps can parse to configure TOTP.
  3. Device clocks drift by seconds. A window of +/- 1 time step (60 seconds total) accommodates typical clock skew without compromising security.
  4. Backup codes are one-time codes generated during setup. They allow access when the user has lost their authenticator device. Each code is single-use.
  5. With 1M possible codes and 30-second Windows, an attacker could brute-force quickly. Rate limiting to 5 attempts/minute makes brute-forcing infeasible.

Challenge: Build a complete TOTP MFA system with QR code provisioning, time-window verification, backup codes, rate limiting, and an endpoint that returns remaining backup codes.

FAQ

Can TOTP work without internet?

Yes. TOTP generates codes locally on the device using the shared secret and the device's clock. No network connectivity is required.

What happens if the user's clock is wrong?

TOTP uses a verification window (+/- 1 time step). If the clock is off by more than 30 seconds, verification fails. Users should enable automatic time sync on their devices.

How do I support multiple authenticator apps?

Each app needs its own secret. Generate separate secrets for each device or use a single secret that can be scanned into multiple apps.

Can TOTP be used for API authentication?

Indirectly. TOTP verifies the user's identity, which then allows the issuance of API tokens. The TOTP code itself is not an API credential.

What is the difference between HOTP and TOTP?

HOTP uses a counter that increments on each use. TOTP uses time instead of a counter. TOTP is more user-friendly (no out-of-sync counter issues).

Is TOTP resistant to phishing?

TOTP alone is not phishing-resistant. A phishing site can ask for the TOTP code and use it immediately. FIDO2/WebAuthn is phishing-resistant.

Mini Project

Build a TOTP MFA service with QR code provisioning, time-window TOTP verification, backup codes with hash storage, rate limiting, and a test script that verifies codes across time steps.

What's Next

Now explore Passwordless Authentication with Magic Links for eliminating passwords entirely from the auth flow.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro