keepcalculator

06 Numbers and Powers

Django OTP Verification: 4 Security Mistakes to Fix

Card No. 06·4298 · Updated · Standard formula, shown below

Django OTP verification often fails because of four common security mistakes: using a weak secret key, not rate-limiting attempts, storing OTPs in plain text, and ignoring time-based expiration. Fixing these issues will make your two-factor authentication (2FA) and password reset flows significantly more resistant to brute-force, replay, and timing attacks. Below, we break down each mistake, explain the correct implementation, and answer related questions.

Mistake 1: Weak or Reused Secret Keys

The secret key is the seed from which every OTP is derived. If you use a short, predictable, or reused secret (e.g., a hardcoded string in your settings), an attacker who obtains it can generate all future codes. Django’s django-otp and pyotp libraries generate random secrets, but developers sometimes override them with a constant for convenience.

The fix: Always generate a fresh, cryptographically random secret for each user using pyotp.random_base32() or secrets.token_hex(). Store it in a field that is encrypted at rest (e.g., using Django’s Fernet or a dedicated encryption library). Never log the secret or include it in URLs. For TOTP (time-based OTP), the secret is a base32-encoded string; for HOTP (counter-based), it’s a random byte string. The secret should be at least 160 bits (20 bytes) to match the security of SHA-1, the default hash.

Mistake 2: No Rate Limiting on Verification

Without rate limiting, an attacker can brute-force a 6-digit OTP by trying all 1,000,000 combinations. With a typical 30-second validity window, a fast script can attempt thousands of codes per minute. Django’s built-in django-ratelimit or django-axes can throttle requests per IP and per user. A common pattern is to allow 5 attempts per OTP, then lock the session for 10 minutes.

The fix: Use django-ratelimit with a key that combines the user ID and the OTP purpose (e.g., @ratelimit(key='user_or_ip', rate='5/m', method='POST')). Also add a delay after each failed attempt using time.sleep(1) to slow down automated guessing. Remember that rate limiting should apply to both the OTP generation endpoint and the verification endpoint.

Mistake 3: Storing OTPs in Plain Text

If you store the OTP itself (rather than a hash) in your database, a data breach exposes every active code. Even if the OTP is short-lived, a dump of your database could allow an attacker to replay codes that are still within their validity window. The same applies to storing the secret key in plain text.

The fix: Never store the OTP value. For TOTP, you don’t need to store the current code; you only store the secret and verify by recomputing the expected code at the current time. For HOTP, store a hash of the counter or the next expected counter value, and use a constant-time comparison (hmac.compare_digest) to prevent timing attacks. If you must store a one-time code for email or SMS, store a SHA-256 hash of the code with a random salt, and set a short expiration (e.g., 5 minutes).

Mistake 4: Ignoring Time-Based Expiration and Replay Protection

OTPs that never expire or that can be reused multiple times are a serious vulnerability. A stolen code could be used hours later, or a user might accidentally reuse an old code that an attacker captured. TOTP codes should expire after 30 seconds (or a multiple of 30), and HOTP counters should advance each time a code is used.

The fix: For TOTP, use pyotp.TOTP(secret).verify(code, valid_window=1) to allow a small clock skew (usually ±1 step). For HOTP, after successful verification, increment the counter in the database. For email/SMS OTPs, store an expiration timestamp and delete the code after a successful or failed attempt. Also, enforce a maximum number of attempts (e.g., 3) before invalidating the code entirely.

How Django OTP Verification Works

Django OTP verification typically follows this flow:

  1. User requests a code (via SMS, email, or authenticator app).
  2. The server generates a secret (for TOTP/HOTP) or a random code, stores it (hashed or as a secret), and sends it to the user.
  3. The user submits the code.
  4. The server recomputes the expected code using the stored secret and the current time (TOTP) or counter (HOTP).
  5. If the submitted code matches, the user is authenticated; otherwise, the attempt is logged and rate-limited.

For TOTP, the standard algorithm (RFC 6238) uses HMAC-SHA1, a time step of 30 seconds, and a 6-digit output. The secret is base32-encoded. For HOTP (RFC 4226), the counter increments with each use, and the code is generated by HMAC-SHA1 on the counter. Both are implemented in the pyotp library, which is the de facto standard in Django projects.

Choosing the Right OTP Method

The best OTP method depends on your threat model and user base:

  • TOTP (Authenticator apps): Most secure for 2FA. No SMS interception risk. Users need a smartphone app like Google Authenticator or Authy.
  • HOTP (Hardware tokens): Good for enterprise, but requires physical tokens and counter management.
  • Email OTP: Convenient but less secure (email accounts can be compromised). Use for low-risk actions like account recovery.
  • SMS OTP: Vulnerable to SIM swapping and SS7 attacks. Avoid for high-security accounts; if used, combine with rate limiting and a short expiration.

For password resets, a single-use, hashed, time-limited code is standard. For 2FA, TOTP is recommended because it doesn’t require sending codes over the network.

Common Implementation Pitfalls

Beyond the four main mistakes, developers often trip on:

  • Clock skew: If the server and client clocks differ by more than 30 seconds, TOTP fails. Use valid_window=1 to allow a 30-second drift.
  • Not using HTTPS: OTPs sent over HTTP can be intercepted. Always enforce HTTPS.
  • Logging OTPs: Never print the code in logs or debug output.
  • Reusing the same secret for multiple purposes: Use separate secrets for 2FA and password reset.
  • Not invalidating after logout: If a user logs out, any pending OTP sessions should be invalidated.

Security Best Practices Beyond the Basics

To harden your Django OTP flow further:

  • Use hmac.compare_digest for all code comparisons to avoid timing attacks.
  • Set a maximum number of attempts (e.g., 3) per OTP and invalidate the code after that.
  • Add CAPTCHA after a few failed attempts to slow down bots.
  • Monitor failed attempts with logging and alerting to detect brute-force attacks.
  • Rotate secrets periodically or when a user changes their phone number.
  • Consider using django-otp with django-two-factor-auth for a full-featured solution.

Frequently Asked Questions

Can I use Django’s built-in auth for OTP?

No, Django’s built-in authentication does not include OTP. You need third-party packages like django-otp or pyotp. These integrate with Django’s auth system and provide the necessary models and views.

What is the difference between TOTP and HOTP?

TOTP (time-based) generates a code based on the current time, valid for 30 seconds. HOTP (counter-based) generates a code based on a counter that increments with each use. TOTP is more common for consumer apps because it doesn’t require storing a counter on the server.

How do I prevent replay attacks with OTP?

For TOTP, ensure the code is only valid for a single use by tracking the last used timestamp or by using a short validity window. For HOTP, increment the counter after each successful verification. For email/SMS codes, delete the code after it’s used or after a few failed attempts.

Should I store the OTP secret in the user model?

It’s common to store the secret in a separate model (e.g., OTPDevice) linked to the user. Encrypt the secret at rest using Django’s Fernet or a similar encryption scheme. Never store it in plain text.

How long should an OTP be valid?

For TOTP, the standard is 30 seconds. For email/SMS, 5-10 minutes is typical. Shorter is more secure but less convenient. Always invalidate the code after a few failed attempts to prevent brute-force.

Is SMS OTP safe enough for banking?

SMS OTP is not recommended for high-security applications due to SIM swapping and SS7 vulnerabilities. Use TOTP with an authenticator app or hardware tokens for banking. If you must use SMS, combine it with rate limiting and transaction monitoring.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →