Skip to content

feat(public-api): add Redis-backed login lockout#164

Open
Nitin-kumar-yadav1307 wants to merge 5 commits into
geturbackend:mainfrom
Nitin-kumar-yadav1307:feature/public-api-login-lockout
Open

feat(public-api): add Redis-backed login lockout#164
Nitin-kumar-yadav1307 wants to merge 5 commits into
geturbackend:mainfrom
Nitin-kumar-yadav1307:feature/public-api-login-lockout

Conversation

@Nitin-kumar-yadav1307
Copy link
Copy Markdown
Contributor

@Nitin-kumar-yadav1307 Nitin-kumar-yadav1307 commented May 9, 2026

Summary

This PR adds per-email login lockout protection in public-api using Redis.

After 5 consecutive failed login attempts for a given project + email, the account is locked for 15 minutes.

Changes

  • Added shared lockout utility in packages/common/src/utils/loginLockout.js
  • Integrated lockout checks and attempt tracking in apps/public-api/src/controllers/userAuth.controller.js
  • Cleared lockout state after successful signup and password reset
  • Hardened the login flow against Redis outages and made lockout reads atomic
  • Added and updated auth tests for lockout behavior

Validation

  • Ran focused public-api auth tests
  • Result: 3 suites passed, 39 tests passed

Summary by CodeRabbit

  • New Features

    • Added account lockout mechanism that temporarily locks accounts after multiple failed login attempts.
    • Lockout is automatically cleared after successful login, signup verification, or password reset.
  • Tests

    • Expanded test coverage for login lockout scenarios and edge cases across authentication flows.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 9, 2026

Warning

Rate limit exceeded

@Nitin-kumar-yadav1307 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 1 second before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e1797d59-7e4e-46b6-9048-68d9cc81bb13

📥 Commits

Reviewing files that changed from the base of the PR and between 5bd019c and 4144d7b.

📒 Files selected for processing (1)
  • apps/public-api/src/controllers/userAuth.controller.js
📝 Walkthrough

Walkthrough

This PR implements Redis-backed login lockout protection across the authentication system. A new lockout utility module provides atomic failure tracking and lock state management; the common package exports these helpers; the auth controller integrates pre-checks, failure recording, and post-success cleanup; and test suites validate the behavior through mocked lockout functions and new assertions.

Changes

Login Lockout Protection

Layer / File(s) Summary
Redis-backed lockout utility with atomic Lua operations
packages/common/src/utils/loginLockout.js
Defines per-project/per-email failure tracking with configurable thresholds (max attempts, lockout duration). Uses embedded Lua to atomically increment failure counters, manage TTLs, and transition to a locked state. Exports checkLockout, recordFailedAttempt, and clearLockout functions.
Shared package exports and cleanup
packages/common/src/index.js
Removes unused model and queue exports (PlatformEvent, DeveloperActivity, activity/reliability queues) and wires lockout helpers into the common package's public interface.
Auth controller lockout integration
apps/public-api/src/controllers/userAuth.controller.js
Adds lockout checks before credential validation in login; records failed attempts and returns 423 when threshold reached; clears lockout state after successful signup/login/password-reset. Handler signature updated to accept next for structured error propagation.
Test suite updates for lockout behavior
apps/public-api/src/__tests__/userAuth.email.test.js, userAuth.refresh.test.js, userAuth.social.test.js
Extends mocks with lockout function stubs (checkLockout, recordFailedAttempt, clearLockout). Adds assertions that lockout state is cleared after successful operations. Introduces new login test cases for locked-account rejection and 423-on-threshold scenarios.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant LoginHandler
  participant LockoutService
  participant Credentials
  participant Response

  Client->>LoginHandler: POST /login
  LoginHandler->>LockoutService: checkLockout(projectId, email)
  alt Already locked
    LockoutService-->>LoginHandler: {locked: true, retryAfterSeconds: N}
    LoginHandler->>Response: 423 Locked Out
  else Not locked
    LoginHandler->>Credentials: validate password
    alt Credential invalid
      LoginHandler->>LockoutService: recordFailedAttempt(projectId, email)
      alt Threshold reached
        LockoutService-->>LoginHandler: {locked: true, retryAfterSeconds: N}
        LoginHandler->>Response: 423 Locked Out
      else Below threshold
        LoginHandler->>Response: 400 Invalid credentials
      end
    else Credential valid
      LoginHandler->>LockoutService: clearLockout(projectId, email)
      LoginHandler->>Response: 200 Login success
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • geturbackend/urBackend#146: Implements the exact Redis-backed per-email login lockout utilities and wires them into the auth controller with pre-checks, failure recording, and success-time clearing.

Possibly related PRs

  • geturbackend/urBackend#160: Identical lockout feature implementation with checkLockout/recordFailedAttempt/clearLockout utilities and auth controller integration.
  • geturbackend/urBackend#162: Directly opposing changes that revert and remove the lockout helpers and their controller/test integrations.
  • geturbackend/urBackend#61: Modifies userAuth.controller.js login/token-issuance paths with session-based refresh token mechanics, sharing code-level overlap with the lockout handling added here.

Suggested labels

level-3

Suggested reviewers

  • yash-pouranik

Poem

🐰 A lockout tale, told with Redis flair
Brute force attempts? They'll find no lair!
Three helpers guard the login gate,
Track failures fast—they won't be late.
Lua scripts dance, atomic and true,
Welcome, dear feature, we needed you! 🔒

🚥 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 'feat(public-api): add Redis-backed login lockout' clearly and concisely summarizes the main change: implementing a Redis-backed login lockout feature in the public API.
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
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Redis-backed per-email login lockout to the public-api authentication flow, aiming to slow brute-force attempts by locking a (projectId, email) pair after repeated failures.

Changes:

  • Introduces a shared loginLockout utility in packages/common using Redis + Lua for atomic lockout checks/updates.
  • Integrates lockout check + failed-attempt tracking into userAuth.controller.js login flow and clears lockout state on successful login/signup/password reset.
  • Updates auth test suites to mock the new common exports and adds login lockout behavior tests.

Reviewed changes

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

Show a summary per file
File Description
packages/common/src/utils/loginLockout.js New Redis/Lua-based lockout utility (check, record failure, clear).
packages/common/src/index.js Re-exports lockout helpers from @urbackend/common.
apps/public-api/src/controllers/userAuth.controller.js Adds lockout enforcement to login; clears lockout after successful signup/login/reset.
apps/public-api/src/tests/userAuth.social.test.js Extends @urbackend/common mock with lockout helpers.
apps/public-api/src/tests/userAuth.refresh.test.js Extends @urbackend/common mock with lockout helpers.
apps/public-api/src/tests/userAuth.email.test.js Adds lockout expectations/tests and updates mocks/imports accordingly.

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

const rawResult = await redis.eval(atomicCheckLua, 1, lockKey, String(LOCKOUT_SECONDS));
const [lockedRaw, ttlRaw] = Array.isArray(rawResult) ? rawResult : [0, 0];
const locked = Number(lockedRaw) === 1;
const retryAfterSeconds = locked ? (Number(ttlRaw) || LOCKOUT_SECONDS) : 0;
Comment on lines +27 to +33
local lockExists = redis.call('GET', lockKey)
if lockExists then
local lockTtl = redis.call('TTL', lockKey)
if lockTtl < 0 then
lockTtl = lockoutSeconds
end
return { maxAttempts, 1, lockTtl }
Comment on lines +1074 to +1080
let lockStatus = { locked: false, retryAfterSeconds: 0 };
try {
lockStatus = await checkLockout(projectId, normalizedEmail);
} catch (lockErr) {
console.error('[login-lockout] checkLockout failed:', lockErr?.message || lockErr);
return sendLockoutServiceError();
}
@yash-pouranik
Copy link
Copy Markdown
Collaborator

@Nitin-kumar-yadav1307
some more

@yash-pouranik yash-pouranik linked an issue May 10, 2026 that may be closed by this pull request
@yash-pouranik
Copy link
Copy Markdown
Collaborator

@Nitin-kumar-yadav1307

@Nitin-kumar-yadav1307 Nitin-kumar-yadav1307 force-pushed the feature/public-api-login-lockout branch from dc50a4f to 5bd019c Compare May 17, 2026 08:25
@yash-pouranik
Copy link
Copy Markdown
Collaborator

you can ignore frontend lint errors
those are on main branch and will be updated in next commit

Copy link
Copy Markdown
Contributor

@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: 1

🧹 Nitpick comments (1)
apps/public-api/src/__tests__/userAuth.email.test.js (1)

370-421: ⚡ Quick win

Add a login test for lockout-service failure branches (503).

Please add cases where checkLockout and recordFailedAttempt reject, and assert the login handler returns the expected service-unavailable response. This validates the outage-hardening behavior introduced in this PR.

🤖 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 370 - 421,
Add two new tests for controller.login that simulate lockout-service failures:
one where checkLockout rejects and one where recordFailedAttempt rejects; in
each test, mock checkLockout or recordFailedAttempt to return a rejected
promise, call controller.login with an appropriate req/res (reuse
makeReq/makeRes), and assert that res.status was called with 503 and that
downstream actions like bcrypt.compare or further recordFailedAttempt calls are
not invoked as appropriate. Locate the existing login tests referencing
checkLockout, recordFailedAttempt, and controller.login to mirror their setup
and assertions so the new tests validate the service-unavailable behavior when
the lockout service throws.
🤖 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 `@apps/public-api/src/controllers/userAuth.controller.js`:
- Around line 1059-1064: sendAuthError currently returns a raw { error: message
} JSON in the fallback path; change it to consistently use the AppError envelope
or the standard API envelope: inside sendAuthError (referencing sendAuthError,
AppError, next, res) always prefer routing errors through next(new
AppError(statusCode, message)); if next is not a function, respond with the
standardized error shape { success: false, data: {}, message } instead of {
error: message } so all login error responses follow the project's contract and
no raw DB/error details are leaked.

---

Nitpick comments:
In `@apps/public-api/src/__tests__/userAuth.email.test.js`:
- Around line 370-421: Add two new tests for controller.login that simulate
lockout-service failures: one where checkLockout rejects and one where
recordFailedAttempt rejects; in each test, mock checkLockout or
recordFailedAttempt to return a rejected promise, call controller.login with an
appropriate req/res (reuse makeReq/makeRes), and assert that res.status was
called with 503 and that downstream actions like bcrypt.compare or further
recordFailedAttempt calls are not invoked as appropriate. Locate the existing
login tests referencing checkLockout, recordFailedAttempt, and controller.login
to mirror their setup and assertions so the new tests validate the
service-unavailable behavior when the lockout service throws.
🪄 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: c009e0a5-6094-4e0e-8a44-895de9241a41

📥 Commits

Reviewing files that changed from the base of the PR and between d45208d and 5bd019c.

📒 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

Comment thread apps/public-api/src/controllers/userAuth.controller.js
@Nitin-kumar-yadav1307
Copy link
Copy Markdown
Contributor Author

that means we done with it ?

@yash-pouranik
Copy link
Copy Markdown
Collaborator

@coderabbitai
What does this pr do? I forgot

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

Tip

For best results, initiate chat on the files or code changes.

@yash-pouranik Sure! Here's a quick summary of what PR #164 does:


🔐 Redis-Backed Login Lockout

This PR adds brute-force protection to the public-api login flow using Redis. Here's the breakdown:

Core behavior:

  • After 5 consecutive failed login attempts for a given project + email combination, the account is locked out for 15 minutes.
  • During lockout, login attempts return HTTP 423 Locked with a retry-after message.

New utility (packages/common/src/utils/loginLockout.js):

  • checkLockout(projectId, email) — checks if an account is currently locked.
  • recordFailedAttempt(projectId, email) — atomically increments the failure counter (via a Lua script) and triggers lockout when the threshold is hit.
  • clearLockout(projectId, email) — removes both the failure counter and lock keys from Redis.

Integration into userAuth.controller.js:

  • Login: checks lockout before authenticating; records failures on bad credentials; clears lockout on success.
  • Signup: clears any existing lockout after successful email verification.
  • Password reset: clears lockout after a successful password update.

Resilience:

  • Redis errors are logged but don't block the auth flow (fail-open on success paths, fail-closed on lockout reads).

Tests:

  • 3 test suites updated, 39 tests passing — covering locked account denial, threshold triggering (423), and user-not-found lockout paths.

@yash-pouranik
Copy link
Copy Markdown
Collaborator

@coderabbaiai
OK to merge?

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Login Brute-Force Protection (Account Lockout)

3 participants