An OTP (One-Time Password) verification system in Python is a way to generate a temporary numeric or alphanumeric code, send it to a user via SMS, email, or authenticator app, and then verify that the user enters the same code within a short time window. You build it by combining a random code generator, a delivery method (like an email SMTP library or an SMS API), and a server-side store that tracks the code and its expiry. This article explains the core components, walks through a simple implementation, and covers security pitfalls you should avoid.
Why Use an OTP System?
OTPs add a second layer of authentication beyond a password. Even if a password leaks, the attacker still needs the current OTP, which changes frequently and is sent to a device the user controls. Common use cases are login flows, password resets, payment confirmations, and two-factor authentication (2FA). Because OTPs are short-lived and single-use, they limit the damage from credential theft and replay attacks.
Core Components of an OTP System
A complete OTP verification system in Python has four parts:
- Generator – creates a random code of a specified length (usually 6 digits).
- Storage – keeps the code, its expiry timestamp, and the number of attempts on the server side.
- Delivery – sends the code to the user via email (SMTP), SMS (Twilio, Vonage, etc.), or an authenticator app (TOTP).
- Verifier – compares the user-supplied code with the stored one, checks expiry and attempt limits, then marks the code as used.
How to Generate a Secure OTP in Python
Use the secrets module, not random, because secrets is cryptographically strong. A simple generator:
import secrets
def generate_otp(length=6):
# Return a numeric string of given length
return ''.join(secrets.choice('0123456789') for _ in range(length))
For a 6-digit code, there are 1,000,000 possible combinations. The entropy (in bits) is length * log2(10), which is about 19.9 bits for 6 digits – enough for a short-lived code, but not for something long-lived. Increase the length to 8 or add letters if the code must remain valid for more than a few minutes.
Storing and Verifying OTPs Securely
Never store the plain OTP in a database. Instead, store a hash (SHA-256 or better) along with the expiry timestamp and a “used” flag. When the user submits a code, hash the input and compare it to the stored hash. This prevents a database leak from exposing active OTPs.
A typical verification flow:
- User requests an OTP (e.g., enters their email).
- Server generates a code, stores its hash with expiry (e.g., 5 minutes), and sends the plain code to the user.
- User enters the code on the website or app.
- Server looks up the most recent hash for that user, checks expiry, compares hashes, and marks it used if successful.
- If the code is wrong, increment an attempt counter. After 3-5 failed attempts, invalidate the code.
Delivering OTPs: Email vs SMS vs Authenticator Apps
Each delivery method has trade-offs:
- Email – Cheap and easy to implement with Python’s
smtpliband a standard email account. But email is not real-time and can be intercepted if the user’s mailbox is compromised. - SMS – Fast and familiar, but costs money per message and relies on a third-party API (Twilio, Vonage, etc.). SMS is also vulnerable to SIM-swapping attacks, so it’s not the strongest 2FA.
- Authenticator app (TOTP) – The most secure for 2FA because the code is generated on the user’s device and never travels over a network. Python libraries like
pyotpimplement the standard TOTP algorithm (RFC 6238).
Common Mistakes and How to Avoid Them
- Using the
randommodule – It’s predictable. Always usesecretsfor security. - Storing plain OTPs – Hash them, just like passwords.
- No expiry – A code should expire after 5–10 minutes. Long expiry increases the window for brute force.
- No rate limiting – Without limiting attempts, an attacker can brute-force a 6-digit code (1 million tries) in a short time. Add a delay or lockout after a few failures.
- Logging the OTP – Never print or log the code. Logs are often less protected than the database.
- Resending OTPs without invalidating old ones – Always invalidate previous codes when a new one is issued.
Security Considerations and Best Practices
Beyond the basics, consider these:
- Use HTTPS – OTPs sent over HTTP can be intercepted in transit.
- Bind the OTP to a session – Store the user ID or session ID with the OTP so it can’t be used on a different account.
- Invalidate after use – A code should be single-use. Once verified, delete or mark it as used.
- Consider TOTP for high-security flows – TOTP codes are time-based and don’t require sending anything, eliminating interception risk.
- Monitor for abuse – Track how many OTPs a user requests per hour. Excessive requests may indicate an attacker trying to spam or brute-force.
Frequently Asked Questions
Is Python’s random module safe for OTPs?
No. The random module is a pseudo-random generator that is not cryptographically secure. An attacker who can observe enough outputs can predict future ones. Use secrets instead.
How long should an OTP be valid?
Typically 5 to 10 minutes. Shorter is safer but may frustrate users. For high-security actions like password resets, 5 minutes is common. For less critical actions, 10 minutes is acceptable.
Should I store the OTP in a database or in memory?
For a small application, an in-memory store like a dictionary or Redis with an expiry key is fine. For a multi-server setup, use a shared store like Redis or a database table. Either way, store only the hash.
How do I prevent brute-force attacks on OTPs?
Limit the number of attempts (e.g., 5) and then invalidate the code. Add a short lockout period (e.g., 1 minute) after each failure. Also, use a longer code (8 digits) if you need more combinations.
Can I use an authenticator app instead of sending OTPs?
Yes. Authenticator apps use TOTP, which is more secure because the code is generated locally and never transmitted. Python’s pyotp library makes it easy to integrate with Google Authenticator, Authy, or similar apps.
What if the user doesn’t receive the OTP email/SMS?
Provide a “Resend” button that issues a new code and invalidates the old one. Also check spam folders for email. For SMS, verify the phone number format and that the SMS gateway is working.
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
- Channel TLD Websites: What They Are & How to Get One5.00 min/km
- Django Class Based Views CRUD REST API
- Most Secure OS for Privacy: 3 Picks by Device
- Ryobi One+ Battery System: Is the Switch Worth It?
- Tmux Terminal Upgrade: Is It Worth It?
- Refurbished Tech Buying Checklist