Skip to content

fix: add OTP brute-force protection and rate limiting#85

Open
24211a05q8-hue wants to merge 5 commits into
kunalverma2512:mainfrom
24211a05q8-hue:fix-otp-rate-limit
Open

fix: add OTP brute-force protection and rate limiting#85
24211a05q8-hue wants to merge 5 commits into
kunalverma2512:mainfrom
24211a05q8-hue:fix-otp-rate-limit

Conversation

@24211a05q8-hue
Copy link
Copy Markdown

@24211a05q8-hue 24211a05q8-hue commented May 22, 2026

OTP Security Enhancement – Brute Force Protection

Summary

This PR improves the security of the OTP verification system by adding protection against brute-force attacks and unlimited OTP attempts.


Changes Made

  • Added OTP attempt tracking per user
  • Implemented progressive warnings for failed attempts:
    • Shows remaining attempts (e.g., 4, 3, 2, 1)
  • Added automatic lockout after 5 failed attempts
  • Implemented 15-minute cooldown period after lockout
  • Created reusable rate limiting middleware (rateLimiter.js)
  • Updated OTP verification logic to enforce security rules

Security Improvements

  • Prevents OTP brute-force attacks
  • Blocks unlimited OTP guessing attempts
  • Adds temporary lockout on suspicious activity
  • Reduces abuse of authentication endpoint

NOTES

This implementation follows standard authentication security practices used in production systems to prevent OTP abuse and ensure safe verification flow.


SCREEN SHOTS

Screenshot 2026-05-22 174229 Screenshot 2026-05-22 174222 Screenshot 2026-05-22 174214 Screenshot 2026-05-22 174206 Screenshot 2026-05-22 174158

Summary by CodeRabbit

  • Security

    • Rate limiting on OTP verification (10 requests per 15 minutes)
    • Account lockout after repeated failed OTP attempts with configurable lock duration
    • Improved handling of duplicate registrations by cleaning up unverified accounts
  • New Features

    • Login responses now include the full persisted user record alongside token
    • GitHub OAuth flow simplified to return a direct successful login with user and token
  • Bug Fixes

    • Validation error messages corrected for authentication request parsing

Review Change Stack

@github-actions
Copy link
Copy Markdown

🚀 PR Received Successfully

Hello @24211a05q8-hue,

Thank you for taking the initiative to contribute to this project.

Please ensure that your PR follows all project guidelines properly before requesting review.

⚠️ Important Instructions

  • Maintain proper code quality and structure
  • Do not make unnecessary changes/files
  • Ensure responsiveness across devices
  • Follow existing project conventions strictly
  • Attach screenshots/videos for UI-related changes
  • Resolve merge conflicts before requesting review
  • Avoid AI-generated low quality PRs or copied implementations

📌 Mandatory for GSSoC'26 Participants

Joining the community group and announcement channel is compulsory for all contributors participating through GSSoC'26.

Failure to follow contribution guidelines may lead to PR rejection.

We appreciate your effort and wish you a great open-source journey ahead. ✨

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

Warning

Review limit reached

@24211a05q8-hue, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 1 review/hour. Refill in 48 minutes and 9 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e0a2b7ac-d40a-4cef-a198-daf6d3e54ced

📥 Commits

Reviewing files that changed from the base of the PR and between b84b299 and 265d552.

📒 Files selected for processing (3)
  • server/models/Otp.js
  • server/modules/auth/repository.js
  • server/modules/auth/service.js
📝 Walkthrough

Walkthrough

Refactors auth: explicit dotenv loading and env validation; OTP model gains failure/lock fields and TTL removed; adds otpLimiter middleware; repository adds OTP safety helpers and uses returnDocument: "after"; service enforces OTP lockout and adjusts login/GitHub flows; routes and validation integrate these changes.

Changes

OTP Verification with Lockout & Rate Limiting

Layer / File(s) Summary
Environment configuration validation
server/config/env.js
Environment variables load from explicit ./.env path, required keys (including GITHUB_*) are validated, a debug snapshot is logged, and default export changes to process.env.
OTP schema with failure tracking and rate limiting middleware
server/models/Otp.js, server/middlewares/rateLimiter.js
OTP schema fields (email, otp, purpose) expanded; failedAttempts (default 0) and lockUntil (default null) track retry/lockout; createdAt TTL removed. otpLimiter limits requests to 10 per 15 minutes with a custom JSON response.
Repository OTP safety methods
server/modules/auth/repository.js
User update methods switch to Mongoose returnDocument: "after". OTP methods reorganized: createOtp initializes failure/lock fields, findOtp removes sort, and new helpers (incrementOtpFailure, lockOtp, resetOtp) manage OTP state.
Service OTP verification with lockout and auth updates
server/modules/auth/service.js
register deletes unverified users and signup OTPs on duplicate email. verifyOtp enforces lockUntil, increments failures, locks after max attempts, and on success resets/deletes OTP, marks user verified, and returns token + user. login returns full persisted user after updating activity.lastActive. GitHub OAuth simplified (jwt state, token exchange, profile fetch, app token).
Routes and validation middleware integration
server/modules/auth/routes.js, server/modules/auth/validation.js
Imports otpLimiter and wires it into /verify-otp before validation. Validation middleware maps Zod failures from result.error.issues for { field, message } responses.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble code in moonlit hops,

I count the tries and block the clops,
Ten tries then pause — the fortress stands,
Envkeys set by careful hands,
GitHub gates now simplified with plans.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main security improvement: adding OTP brute-force protection and rate limiting, which is the primary focus of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/config/env.js`:
- Around line 6-18: The env validation list in server/config/env.js is missing
the GitHub OAuth variables used by AuthService.getGithubAuthorizationUrl() and
AuthService.handleGithubCallback(); add "GITHUB_CLIENT_ID",
"GITHUB_CLIENT_SECRET", and "GITHUB_CALLBACK_URL" to the requiredEnvVars array
so startup fails fast when those env vars are absent (keep the existing
validation/exit behavior intact).

In `@server/models/Otp.js`:
- Around line 32-35: The Otp model currently defines createdAt without a TTL and
the auth flows (repository.findOtp, service.verifyOtp, service.resetPassword)
don’t enforce expiry; update the Otp schema (createdAt) to include an expires
TTL (or create a TTL index) using the configured OTP lifetime, and additionally
enforce expiry in repository.findOtp (or in verifyOtp/resetPassword) by
filtering on createdAt (e.g., only return OTPs newer than now - OTP_TTL) so
stale OTP docs cannot be used; also ensure createOtp behavior (Otp.deleteMany)
remains but is not the only mechanism relying on expiry.

In `@server/modules/auth/repository.js`:
- Around line 76-88: createOtp currently deletes-then-saves which is non-atomic;
add a unique compound index on { email, purpose } in server/models/Otp.js (e.g.,
schema.index({ email: 1, purpose: 1 }, { unique: true })) and change createOtp
to perform a single atomic upsert using Otp.findOneAndUpdate({ email, purpose },
{ $set: { otp, failedAttempts: 0, lockUntil: null } }, { upsert: true, new:
true, setDefaultsOnInsert: true }) instead of Otp.deleteMany + save; this
ensures deterministic behavior for findOtp and prevents duplicate documents
under concurrent requests.

In `@server/modules/auth/service.js`:
- Around line 466-469: The jwt.verify call in handleGithubCallback is discarding
the decoded payload (the signed "state" containing mode and userId) so the
callback can't differentiate connect vs login flows; change the jwt.verify usage
in handleGithubCallback (and the similar block covering lines ~507-525) to
capture the decoded token result (e.g., const decoded = jwt.verify(state,
process.env.JWT_SECRET)) and then read decoded.mode and decoded.userId to drive
the /github/connect vs login behavior, preserving existing error handling and
expiry checks.
- Around line 507-517: The code currently calls generateAccessToken using
possibly undefined claims after AuthRepository.findUserByGithubId returns null;
update the logic around AuthRepository.findUserByGithubId / generateAccessToken
so that you first check if user is null/undefined and, if so, fail early
(respond/throw with a 401 or 404 as your API convention dictates) instead of
minting an app token; only call generateAccessToken and proceed to return
success when user is present (use the existing user?._id, user?.email,
user?.role only after confirming user exists).
- Around line 389-403: The resendOtp flow (resendOtp) deletes and recreates the
OTP record via AuthRepository.deleteOtp and AuthRepository.createOtp which
resets failedAttempts and lockUntil and allows bypassing the cooldown; change
the logic to preserve lockout state by first reading the existing OTP (e.g.,
AuthRepository.getOtp or fetching by email/purpose), then either update the
existing record's otp/hash and expiry (leave failedAttempts and lockUntil
untouched) or, if you must recreate, carry over failedAttempts and lockUntil
into the new create payload; also ensure generateOTP and bcrypt.hash are used
only after confirming the user is not currently locked (lockUntil in the future)
to prevent issuing a new OTP during lockout.
- Around line 316-339: The forgot-password OTP flow (AuthRepository.findOtp ->
otpRecord -> bcrypt.compare) currently skips brute-force protections: before
calling bcrypt.compare check otpRecord.lockUntil and if lockUntil > now throw
the same ApiError; after a failed bcrypt.compare increment the record's
failedAttempts via the repository (e.g., AuthRepository.incrementFailedAttempts
or AuthRepository.updateOtp) and if failedAttempts reaches the configured
threshold set otpRecord.lockUntil to now + lockDuration and persist that change;
on a successful compare reset failedAttempts (and clear lockUntil) and persist;
ensure all updates use the AuthRepository methods so the lockout behaviour
mirrors the signup verification flow and still throws ApiError(400, "Invalid or
expired OTP") for failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 65cb400e-7875-4c2f-bc9a-903bd077c0e9

📥 Commits

Reviewing files that changed from the base of the PR and between 47f72c6 and c323d1d.

⛔ Files ignored due to path filters (1)
  • server/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • server/config/env.js
  • server/middlewares/rateLimiter.js
  • server/models/Otp.js
  • server/modules/auth/repository.js
  • server/modules/auth/routes.js
  • server/modules/auth/service.js
  • server/modules/auth/validation.js

Comment thread server/config/env.js
Comment thread server/models/Otp.js
Comment thread server/modules/auth/repository.js Outdated
Comment thread server/modules/auth/service.js
Comment thread server/modules/auth/service.js Outdated
Comment thread server/modules/auth/service.js Outdated
Comment thread server/modules/auth/service.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant