Skip to content

Revert "Add Redis-backed per-email login lockout after 5 failures" - #162

Merged
yash-pouranik merged 1 commit into
mainfrom
revert-160-feature/public-api-login-lockout
May 9, 2026
Merged

Revert "Add Redis-backed per-email login lockout after 5 failures"#162
yash-pouranik merged 1 commit into
mainfrom
revert-160-feature/public-api-login-lockout

Conversation

@yash-pouranik

@yash-pouranik yash-pouranik commented May 9, 2026

Copy link
Copy Markdown
Member

Reverts #160

Summary by CodeRabbit

  • Changes
    • Account lockout functionality has been removed from the authentication system. Failed login attempts will no longer trigger temporary account restrictions.

Review Change Stack

Copilot AI review requested due to automatic review settings May 9, 2026 09:31
@yash-pouranik
yash-pouranik temporarily deployed to revert-160-feature/public-api-login-lockout - urBackend-frankfrut PR #162 May 9, 2026 09:31 — with Render Destroyed
@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR removes the Redis-based login lockout feature from the authentication system. The change deletes the lockout utility module, removes lockout function exports from the common package, simplifies the login controller to skip lockout checks and failed-attempt tracking, and updates all related test mocks and assertions to reflect the removal.

Changes

Remove Login Lockout Feature

Layer / File(s) Summary
Lockout Utility Deletion
packages/common/src/utils/loginLockout.js
Entire module deleted: Redis key generation, Lua scripts for atomic failure tracking, checkLockout(), recordFailedAttempt(), and clearLockout() functions removed.
Common Package Exports
packages/common/src/index.js
Removed exports for checkLockout, recordFailedAttempt, clearLockout; added AppError, sessionManager, and planLimits exports.
Controller Implementation
apps/public-api/src/controllers/userAuth.controller.js
Removed lockout imports and function calls; login endpoint signature changed from async (req, res, next) to async (req, res); login now only validates credentials without lockout checks or attempt tracking; signup and password-reset handlers no longer clear lockout state.
Test Mocks & Assertions
apps/public-api/src/__tests__/userAuth.email.test.js, apps/public-api/src/__tests__/userAuth.refresh.test.js, apps/public-api/src/__tests__/userAuth.social.test.js
Removed lockout-related Jest mock definitions; removed assertions for clearLockout calls in signup and reset flows; removed assertions for checkLockout and recordFailedAttempt in login flows; deleted test cases covering locked-account and lockout-threshold scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • geturbackend/urBackend#61: Modifies the same userAuth controller login/signup flows and removes middleware pattern from auth endpoints.
  • geturbackend/urBackend#160: Directly conflicts with this PR by adding lockout helpers and exports to the same controller and common package files.
  • geturbackend/urBackend#128: Modifies the same common package exports (packages/common/src/index.js) to add AppError and plan-related utilities.

Suggested labels

NSOC'26, level-1

Poem

🐰 The locks are gone, the counts set free,
No more Redis keys in our auth spree,
Simple login flows, no failed attempts to track,
The lockout burden off our back! 🔓

🚥 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 accurately describes the main change: reverting a previous feature that added Redis-backed login lockout functionality. It is specific, concise, and directly related to all the changes shown (removal of lockout code, mocks, and controller logic).
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch revert-160-feature/public-api-login-lockout

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Reverts the previously added Redis-backed, per-email login lockout (5 failures → 15 minute lock) by removing the shared lockout utility and backing out its usage in the Public API auth flows and tests.

Changes:

  • Deletes the loginLockout Redis/Lua-based utility from packages/common.
  • Removes checkLockout / recordFailedAttempt / clearLockout exports from @urbackend/common.
  • Backs out lockout enforcement/cleanup in userAuth.controller and updates Jest mocks/tests accordingly.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/common/src/utils/loginLockout.js Removes the Redis-backed per-email lockout implementation entirely.
packages/common/src/index.js Removes lockout utility exports from the common package surface.
apps/public-api/src/controllers/userAuth.controller.js Removes lockout checks and failed-attempt tracking from signup/login/reset-password flows.
apps/public-api/src/tests/userAuth.social.test.js Updates common-module mocks to no longer include lockout functions.
apps/public-api/src/tests/userAuth.refresh.test.js Updates common-module mocks to no longer include lockout functions.
apps/public-api/src/tests/userAuth.email.test.js Removes lockout-related mocks/assertions and drops lockout-specific test cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1049 to 1067
@@ -1089,43 +1060,10 @@ module.exports.login = async (req, res, next) => {

const user = await Model.findOne({ email: normalizedEmail }).select('+password');

if (!user) {
let failedStatus = { locked: false, retryAfterSeconds: 0, attempts: 0 };
try {
// Fail-open: if Redis is unavailable, do not block login on attempt tracking.
failedStatus = await recordFailedAttempt(projectId, normalizedEmail);
} catch (attemptErr) {
console.error('[login-lockout] recordFailedAttempt failed (user missing):', attemptErr?.message || attemptErr);
}

if (failedStatus.locked) {
return sendAuthError(423, `Account temporarily locked. Try again in ${failedStatus.retryAfterSeconds} seconds.`);
}
return sendAuthError(400, 'Invalid email or password');
}
if (!user) return res.status(400).json({ error: "Invalid email or password" });

const validPass = await bcrypt.compare(password, user.password);
if (!validPass) {
let failedStatus = { locked: false, retryAfterSeconds: 0, attempts: 0 };
try {
// Fail-open: if Redis is unavailable, do not block login on attempt tracking.
failedStatus = await recordFailedAttempt(projectId, normalizedEmail);
} catch (attemptErr) {
console.error('[login-lockout] recordFailedAttempt failed (invalid password):', attemptErr?.message || attemptErr);
}

if (failedStatus.locked) {
return sendAuthError(423, `Account temporarily locked. Try again in ${failedStatus.retryAfterSeconds} seconds.`);
}
return sendAuthError(400, 'Invalid email or password');
}

try {
// Fail-open: if Redis is unavailable, do not block successful login.
await clearLockout(projectId, normalizedEmail);
} catch (clearErr) {
console.error('[login-lockout] clearLockout failed:', clearErr?.message || clearErr);
}
if (!validPass) return res.status(400).json({ error: "Invalid email or password" });

Comment on lines 94 to 96
const sessionManager = require("./utils/session.manager");
const planLimits = require("./utils/planLimits");
const AppError = require("./utils/AppError");

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/public-api/src/controllers/userAuth.controller.js (1)

1049-1066: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Reverting per-email login lockout reintroduces a brute-force / credential-stuffing exposure on /api/userAuth/login.

After this revert, a failed login simply returns 400 Invalid email or password with no per-email throttling, no failed-attempt tracking, and no lockout. I don't see any compensating control on this route in this file (no rate-limit middleware, no CAPTCHA), and assertRefreshRateLimits is wired only into refreshToken — not login. Combined with the consistent error message for "user not found" vs "wrong password" (which is otherwise good), an attacker can iterate password guesses against any known email at full request rate.

The PR description is just "Reverts #160" with no rationale. Before merging, please confirm at least one of the following:

  1. There is an equivalent compensating control already in front of this route (e.g., express-rate-limit / WAF / API-gateway throttle keyed on email or IP+email) — and link to it in the PR body.
  2. The original lockout had a concrete defect that justified an emergency revert, and a follow-up issue is tracked to re-land protection.
  3. Otherwise, keep the lockout (or land a slimmer fix) rather than fully reverting.

If the revert is being done because the lockout was itself abusable (e.g., attacker-induced denial of service by locking victim accounts), that's worth fixing forward (lock by IP+email tuple, exponential backoff, sliding window) rather than removing the control entirely.

Want me to draft a minimal express-rate-limit-based per-email throttle for this route, or open a tracking issue for re-landing lockout with the abuse-resistance hardening above?

🤖 Prompt for 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.

In `@apps/public-api/src/controllers/userAuth.controller.js` around lines 1049 -
1066, The login route (module.exports.login) currently has no per-email/IP
throttling after the revert, reintroducing brute-force risk; either (A) restore
the previous per-email lockout/failed-attempt tracking logic used before the
revert (re-enable the code that increments failed attempts on the user record or
cache and enforces temporary lockout inside login and resets on successful
auth), or (B) add a slim express-rate-limit middleware applied to
/api/userAuth/login that keys limits by normalizedEmail (and optionally
IP+email) to throttle guesses; if an external mitigation already exists, state
its location in the PR instead of changing code and link to it (note:
assertRefreshRateLimits is only used in refreshToken — do not rely on it for
login).
🧹 Nitpick comments (1)
apps/public-api/src/__tests__/userAuth.email.test.js (1)

318-361: 💤 Low value

Test alignment looks correct, but security coverage on login is now thin.

The mock cleanup (Line 102) and the removal of checkLockout / recordFailedAttempt / clearLockout assertions correctly mirror the controller revert — no orphaned expectations remain. That said, the login describe now only covers the happy path and a single wrong-password case; there is no test that would catch a regression in any future brute-force protection on this endpoint.

If the controller-side concern about restoring a compensating control is addressed (rate limit / re-landed lockout / etc.), please bring back a focused test (e.g., "Nth failed attempt is throttled / locked") at that point. No change needed in this PR purely for the revert mechanics.

🤖 Prompt for 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.

In `@apps/public-api/src/__tests__/userAuth.email.test.js` around lines 318 - 361,
The test suite for the login endpoint now only covers success and a single
wrong-password case and lacks coverage for brute-force/lockout behavior; when
you reintroduce any compensating control (e.g., rate-limit or account lockout)
in the controller.login flow, add a focused test in this file (inside the
describe('login') block) that simulates repeated failed logins and asserts the
lockout/throttle behavior: use makeReq/makeRes and mockModel.findOne to return
the user, mock bcrypt.compare to return false repeatedly, and assert that the
controller calls the lockout-related helpers (checkLockout, recordFailedAttempt,
clearLockout) or returns the throttled/locked response (e.g., status indicating
lockout) once the Nth failed attempt threshold is reached.
🤖 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.

Outside diff comments:
In `@apps/public-api/src/controllers/userAuth.controller.js`:
- Around line 1049-1066: The login route (module.exports.login) currently has no
per-email/IP throttling after the revert, reintroducing brute-force risk; either
(A) restore the previous per-email lockout/failed-attempt tracking logic used
before the revert (re-enable the code that increments failed attempts on the
user record or cache and enforces temporary lockout inside login and resets on
successful auth), or (B) add a slim express-rate-limit middleware applied to
/api/userAuth/login that keys limits by normalizedEmail (and optionally
IP+email) to throttle guesses; if an external mitigation already exists, state
its location in the PR instead of changing code and link to it (note:
assertRefreshRateLimits is only used in refreshToken — do not rely on it for
login).

---

Nitpick comments:
In `@apps/public-api/src/__tests__/userAuth.email.test.js`:
- Around line 318-361: The test suite for the login endpoint now only covers
success and a single wrong-password case and lacks coverage for
brute-force/lockout behavior; when you reintroduce any compensating control
(e.g., rate-limit or account lockout) in the controller.login flow, add a
focused test in this file (inside the describe('login') block) that simulates
repeated failed logins and asserts the lockout/throttle behavior: use
makeReq/makeRes and mockModel.findOne to return the user, mock bcrypt.compare to
return false repeatedly, and assert that the controller calls the
lockout-related helpers (checkLockout, recordFailedAttempt, clearLockout) or
returns the throttled/locked response (e.g., status indicating lockout) once the
Nth failed attempt threshold is reached.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 902abd06-4c49-4815-8646-596fdf404f4c

📥 Commits

Reviewing files that changed from the base of the PR and between 37557c8 and 3ebca2b.

📒 Files selected for processing (6)
  • apps/public-api/src/__tests__/userAuth.email.test.js
  • apps/public-api/src/__tests__/userAuth.refresh.test.js
  • apps/public-api/src/__tests__/userAuth.social.test.js
  • apps/public-api/src/controllers/userAuth.controller.js
  • packages/common/src/index.js
  • packages/common/src/utils/loginLockout.js
💤 Files with no reviewable changes (4)
  • apps/public-api/src/tests/userAuth.refresh.test.js
  • packages/common/src/index.js
  • apps/public-api/src/tests/userAuth.social.test.js
  • packages/common/src/utils/loginLockout.js

@yash-pouranik
yash-pouranik merged commit 5e73711 into main May 9, 2026
13 checks passed
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.

2 participants