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

Adds Redis-backed per-email login lockout (projectId+email). Key fixes since initial push:

  • Ensure TTL self-heal: Lua scripts now EXPIRE lock keys when TTL < 0 to avoid indefinite locks.
  • Robust TTL handling in JS: use explicit numeric checks for retryAfterSeconds.
  • Standardize auth error responses: sendAuthError now routes via next(new AppError(...)) or falls back to { success: false, data: {}, message }.
  • Restore exports in @urbackend/common for PlatformEvent, DeveloperActivity, and activity/reliability queue helpers to avoid consumer breakages.
  • Rebased feature branch onto main, resolved conflicts, and force-pushed updated branch.

All focused public-api auth tests pass locally (22 tests).

Requesting review and merge when convenient.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 9, 2026

📝 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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

yash-pouranik commented May 17, 2026

@coderabbitai
OK to merge?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

Tip

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

🧩 Analysis chain

Let me check the current state of the PR before giving a recommendation.


🏁 Script executed:

#!/bin/bash
# Get PR details
gh pr view 164 --repo geturbackend/urBackend --json state,reviewDecision,statusCheckRollup,reviews,comments

# Get open review comments
gh pr view 164 --repo geturbackend/urBackend --json reviewThreads

Length of output: 37665


@yash-pouranik Here's my assessment before merging:


✅ Passing

  • All relevant CI checks pass (public-api-tests, dashboard-api-tests, sdk-tests, python-sdk-tests, CodeQL, CodeRabbit)
  • The frontend-lint failure is pre-existing on main (as you noted)
  • Core feature logic is solid — atomic Lua script, fail-open on success paths, fail-closed on lockout reads, 39 tests passing

⚠️ Items to verify before merging

1. Unresolved actionable comment (mine)

In userAuth.controller.js (~line 1059–1064): sendAuthError uses an inconsistent { error: message } shape in the fallback path vs. the project's standard { success: false, data: {}, message } envelope. This could leak raw error details.

2. Removed exports in packages/common/src/index.js
The diff drops these exports entirely:

  • PlatformEvent, DeveloperActivity
  • activityRollupQueue, scheduleActivityRollup, initActivityRollupWorker
  • reliabilityAlertQueue, scheduleReliabilityAlert, initReliabilityAlertWorker

If any other service/app in the monorepo imports these from @urbackend/common, they will break at runtime. Worth confirming these are truly unused before merging to main.

3. Duplicate / opposing PRs open

  • #160 appears to be an identical implementation of the same lockout feature
  • #162 is open and reverts the lockout helpers

It's worth making sure #160 and #162 are closed/resolved before this lands to avoid confusion.


Bottom line: The feature itself looks good to merge once the removed exports are confirmed safe and the sendAuthError response shape is aligned. If you're comfortable with those two points, it's ready. 🐰

@yash-pouranik
Copy link
Copy Markdown
Collaborator

@Nitin-kumar-yadav1307
2 more remaining??

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