Designing Secure Password Reset & OTP Verification: Timing Attacks, Rate Limits & State
Architecting bulletproof password recovery: secure token hashing, timing attack mitigation with timingSafeEqual, OTP brute-force limits, and single-use invalidation.
By Amr Samir• August 19, 2026• 1 min
Designing Secure Password Reset & OTP Verification: Timing Attacks, Rate Limits & State
1. The Real Attack Vectors in Password Recovery
A naive password reset endpoint is one of the most frequently exploited attack surfaces in web applications. Common vulnerabilities include:
- Brute-Force Enumeration: Submitting 6-digit OTP codes in automated bursts.
- Timing Attacks: Measuring response time differences between registered and non-registered emails.
- Token Replay & State Leaks: Re-using an OTP after password modification or leaking tokens in query strings.
code
[ Attacker ] ---> (Automated 1000 OTPs/sec) ---> [ Vulnerable Reset API ] ---> 💥 Account Takeover!
2. Secure OTP State Machine & Cryptographic Architecture
code
+--------------+ +---------------+ +---------------+
| 1. Request | -> Token -> | 2. Verification| -> Session -> | 3. Reset |
| Email / Phone| Hashed | 6-Digit OTP | One-Time | New Password |
+--------------+ +---------------+ +---------------+
| | |
v v v
Rate Limit: Attempts <= 5, Hash with
Max 3 / hour TTL = 5 mins Argon2id
3. Mitigating Timing Attacks (Constant-Time Verification)
When comparing tokens or searching for user records, prevent side-channel timing analysis using crypto.timingSafeEqual:
typescript
import * as crypto from 'crypto';
export function secureCompare(a: string, b: string): boolean {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
if (bufA.length !== bufB.length) {
// Perform dummy comparison to equalize timing
crypto.timingSafeEqual(bufA, bufA);
return false;
}
return crypto.timingSafeEqual(bufA, bufB);
}
4. Production Rate Limiting & Hashing OTPs
Never store plain-text OTPs in database columns or Redis caches. Store their cryptographic hashes (e.g., SHA-256):
typescript
@Injectable()
export class PasswordResetService {
async initiateReset(email: string) {
const user = await this.usersService.findByEmail(email);
// Generic response regardless of whether user exists to prevent email enumeration
const genericResponse = { message: 'If the email exists, a verification code has been dispatched.' };
if (!user) return genericResponse;
const rateKey = `rate:otp:${user.id}`;
const requestsCount = await this.redis.incr(rateKey);
if (requestsCount === 1) await this.redis.expire(rateKey, 3600); // 1 hour window
if (requestsCount > 3) throw new BadRequestException('Too many reset attempts. Please try again later.');
// Generate cryptographically strong 6-digit OTP
const rawOtp = crypto.randomInt(100000, 999999).toString();
const hashedOtp = crypto.createHash('sha256').update(rawOtp).digest('hex');
await this.redis.set(`reset:otp:${user.id}`, hashedOtp, 'EX', 300); // 5 minutes TTL
await this.notificationQueue.sendOtpEmail(user.email, rawOtp);
return genericResponse;
}
}
5. Security Checklist
- Invalidate all active user sessions/JWT tokens upon successful password reset.
- Set a strict maximum attempt limit (e.g., 5 failed attempts locks the OTP immediately).
- Ensure reset tokens are strictly single-use.
Recommended Posts
Related Projects
Qader - Accessible Examination & Learning Diagnostic System
An educational examination and assessment web application engineered with React and TypeScript for simulated timed tests, resilient local storage session recovery, and academic skill diagnostic reporting.