Skip to content

fix(auth): add per-email rate limiting to resend-otp endpoint#29

Open
algojogacor wants to merge 1 commit into
voiceyBill:mainfrom
algojogacor:fix/resend-otp-rate-limit
Open

fix(auth): add per-email rate limiting to resend-otp endpoint#29
algojogacor wants to merge 1 commit into
voiceyBill:mainfrom
algojogacor:fix/resend-otp-rate-limit

Conversation

@algojogacor
Copy link
Copy Markdown

Summary

Fixes #26POST /auth/resend-otp had no effective rate limiting, allowing unlimited OTP emails to be triggered for any email address.

Problem

The endpoint was protected only by express-rate-limit (IP-based), which can be trivially bypassed by rotating IP addresses or using proxies. An attacker could loop-call the endpoint and:

  • Exhaust the Resend email sending quota
  • Spam a victim's inbox with OTP emails

Solution

Added per-email cooldown tracking at the database level:

  1. User model — Added lastOtpResentAt: Date field (not selected by default)
  2. resendOtpService — Before issuing a new OTP, checks if a previous resend was within the last 60 seconds. If so, returns a descriptive error with the remaining wait time.

Key design decisions

  • 60-second cooldown — balances UX (legitimate users can retry quickly) with abuse prevention
  • Database-level tracking — not bypassable via IP rotation
  • Reuses existing AUTH_TOO_MANY_ATTEMPTS error code — consistent with the project's error conventions
  • Existing express-rate-limit middleware remains — defense in depth

Changes

File Change
src/models/user.model.ts Add lastOtpResentAt field to UserDocument interface and schema
src/services/auth.service.ts Add 60s cooldown check + update lastOtpResentAt on successful resend

Testing

  • Request resend-otp → succeeds
  • Request again within 60s → returns error with retry-after seconds
  • Wait 60s → request succeeds again
  • IP-based rate limiter still enforces global limits

Screenshots

N/A — backend-only change.

Prevent OTP email abuse by enforcing a 60-second cooldown between
resend requests tracked per user via the lastOtpResentAt field.

Previously, the endpoint only used IP-based rate limiting via
express-rate-limit, which can be bypassed by rotating IP addresses.
This adds a database-level cooldown that prevents the same email
from triggering unlimited OTP emails regardless of source IP.

Changes:
- Add lastOtpResentAt field to User model (select: false)
- Enforce 60s cooldown in resendOtpService before issuing new OTP
- Return descriptive error with retry-after seconds on cooldown hit
- Use existing AUTH_TOO_MANY_ATTEMPTS error code for consistency

Closes voiceyBill#26
@vercel
Copy link
Copy Markdown

vercel Bot commented May 15, 2026

@algojogacor is attempting to deploy a commit to the voiceyBill's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the backend Changes backend source code label May 15, 2026
@voiceyBill
Copy link
Copy Markdown
Owner

@algojogacor Please make sure all workflow runs/checks are passing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Changes backend source code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: POST /auth/resend-otp has no rate limiting, unlimited emails can be triggered

2 participants