A six-digit code may look simple to users, but the system behind it needs careful security design. Email-based OTP verification affects account creation, passwordless login, account recovery, and sensitive user actions. If the flow is weak, attackers may abuse it; if it is confusing, genuine users may abandon it.
An email confirmation checks access to an address for that attempt; it is not proof of a person's identity or phishing-resistant login MFA. NIST SP 800-63B-4 prohibits email for out-of-band authentication, while distinguishing email-address confirmation and recovery codes from that authentication requirement. Choose authentication methods appropriate to the action's risk. Public inboxes cannot provide private control of an email factor.
A secure OTP system is not just about generating a six-digit code. Developers need to think about how the code is generated, stored, delivered, expired, verified, retried, logged, and invalidated. The complete flow matters.
If you also need to verify how these emails behave end to end, our guide to QA testing email workflows covers trigger mapping, delivery timing, expiry checks, and inbox testing patterns.
Generate OTPs securely
Do not use predictable random functions for OTP generation. Standard random utilities may be fine for UI effects or non-security use cases, but OTPs are security-sensitive. Use cryptographically secure random generation provided by your backend language or framework. In Python, use the secrets module. In Node.js, use the crypto module.
A six-digit code is a common example, but code length is a design choice rather than proof of security. Consider the allowed attempts, expiry window, and sensitivity of the action together. Longer codes may reduce guessing risk, but they do not turn email into phishing-resistant authentication.
Store only what is necessary
Avoid ordinary plaintext storage of active OTPs and keep only the minimum validation state. Hashing can reduce accidental exposure, but a six-digit code has a small keyspace and can be brute-forced offline. A plain hash alone does not strongly protect a short code after database theft. Use appropriate secret handling alongside short validity and strict server-side attempt limits.
The OTP record should contain enough information to validate the request, but not unnecessary personal data. Keep the storage simple and short-lived. OTP data should not remain in the database after it expires or after it has been used.
Use a short expiry window
Choose a short validity window based on your documented risk and delivery behavior. Check expiry on the server, not only in a browser countdown. A delayed email must not make an expired code valid again. An example number of minutes is a design choice, not a universal security requirement.
The user interface should clearly explain when the code expires. If a user enters an expired OTP, show a helpful message and provide an option to request a new code. Do not silently fail or show generic errors that make the user repeat the same mistake.
Make OTPs single-use
An OTP should become invalid immediately after successful verification. Even if the expiry time has not passed, the same code should not work again. This protects against replay and prevents confusion if users click back or retry the same action.
When a new OTP is generated, decide whether older active codes should remain valid. In most cases, the safest and clearest approach is to invalidate previous codes and allow only the newest code. This reduces confusion when users request multiple codes.
Rate limit both requests and attempts
OTP systems need two types of rate limits. First, rate limit how often a user can request a new OTP. Without this, attackers can spam a user's inbox or increase your email sending costs. Second, rate limit verification attempts so attackers cannot brute-force codes.
Rate limits can use account, email address, IP address, or other signals. Limit both requests and verification attempts on the server. Requiring a fresh code must not let a user reset an account's attempt limit indefinitely; preserve the relevant limits across resends and test that replacement codes cannot bypass them.
Design safe resend behaviour
Users often click resend if an email is delayed. Your resend logic should be clear and safe. Show a short countdown before allowing another request. Let users know that the newest code replaces the old one. Avoid sending multiple active codes that all work at the same time.
It also helps to show the masked email address receiving the OTP, such as a***@example.com. This allows users to confirm they are checking the correct inbox without exposing the full address unnecessarily.
Avoid leaking information in errors
Error messages should be helpful but not reveal sensitive information. For login or account recovery flows, avoid messages that clearly confirm whether an email address exists in your system. Instead of saying 'This email is not registered', use safer wording such as 'If this email exists, we will send a code.'
For verification attempts, messages like 'Invalid or expired code' are usually safer than explaining exactly which part failed. Internally, logs can capture more detail for debugging, but the user-facing message should not help attackers refine their attempts.
Log carefully
Do not log OTP values in application logs, browser logs, analytics events, error tracking systems, or support dashboards. Logs are often accessible to more people and systems than production databases. If OTPs appear in logs, they become a security risk.
Instead, log safe metadata such as request time, success or failure, masked email, attempt count, and general error codes. This gives developers enough information to troubleshoot without exposing the secret itself.
Test with temporary inboxes safely
During development and QA, temporary inboxes are useful for checking whether OTP emails are delivered, formatted correctly, and easy to understand. Use synthetic accounts and non-sensitive data only. Public inboxes should never receive production user OTPs or private account information.
A strong OTP system balances security and usability. Generate codes securely, expire them quickly, invalidate them after use, rate limit aggressively, avoid leaking information, and test the complete user journey. When these pieces work together, email OTP verification becomes safer and easier for users to trust.
