Skip to content

fix(auth): prevent concurrent signup race conditions and lost updates#552

Open
Namraa310806 wants to merge 2 commits into
FireFistisDead:masterfrom
Namraa310806:fix/concurrent-signup-race-condition
Open

fix(auth): prevent concurrent signup race conditions and lost updates#552
Namraa310806 wants to merge 2 commits into
FireFistisDead:masterfrom
Namraa310806:fix/concurrent-signup-race-condition

Conversation

@Namraa310806

@Namraa310806 Namraa310806 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes a race condition in the user registration flow that could cause lost updates and inconsistent user data during concurrent signup requests.

Previously, the authentication system used a read-modify-write pattern on users.json without synchronization. Multiple simultaneous registration requests could read the same file state, perform independent modifications, and overwrite each other's changes when writing back to disk.

This update introduces file locking, atomic writes, and concurrency-safe registration handling to ensure reliable user persistence under concurrent workloads.


Changes Made

Concurrency-Safe User Registration

  • Added file locking using proper-lockfile.
  • Ensured only one signup operation can modify users.json at a time.
  • Prevented concurrent write conflicts during registration.

Duplicate User Validation Under Lock

Moved duplicate-email validation inside the protected critical section.

This ensures:

  • The latest file state is always checked.
  • Concurrent signup requests cannot bypass duplicate checks.
  • User uniqueness is maintained under load.

Atomic File Writes

Implemented atomic write operations using:

users.json.tmp → rename → users.json

Benefits:

  • Prevents partial writes.
  • Reduces risk of file corruption.
  • Improves reliability during unexpected crashes or interruptions.

Lock Cleanup & Error Handling

Added safe lock release logic for:

  • Successful registrations
  • Validation failures
  • Unexpected exceptions

This prevents stale locks and improves recovery behavior.

Regression Test Coverage

Added concurrent registration tests covering:

  • Multiple simultaneous signup requests
  • Lost update prevention
  • Duplicate email handling
  • File consistency verification

Reliability Impact

This change improves:

  • User data consistency
  • Concurrent request handling
  • Registration reliability
  • File integrity during writes

The application can now safely process concurrent signup requests without risking lost registrations or corrupted user data.


Security Impact

This change reduces the risk of:

  • Race-condition exploits
  • Duplicate account creation through concurrent requests
  • Data integrity violations
  • Authentication inconsistencies caused by lost updates

Files Modified

src/controllers/authController.js
Authentication test suite

Verification Checklist

  • File locking implemented
  • Duplicate checks performed under lock
  • Atomic file writes added
  • Lock cleanup implemented
  • Concurrent registration tests added
  • Lost-update vulnerability resolved
  • Existing authentication functionality preserved
  • No breaking API changes introduced

Related Issue

Fixes #503

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved data consistency and reliability during user registration operations
  • Tests

    • Expanded signup endpoint test coverage with comprehensive integration tests for edge cases and concurrent scenarios
  • Chores

    • Updated dependencies to enhance system reliability

@vercel

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

@Namraa310806 is attempting to deploy a commit to the firefistisdead's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Namraa310806, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 35 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: aab071c5-b523-4507-bafb-dcc6075ab925

📥 Commits

Reviewing files that changed from the base of the PR and between e1380ac and 73de0dd.

📒 Files selected for processing (3)
  • server.js
  • src/controllers/authController.js
  • src/data/users.json
📝 Walkthrough

Walkthrough

The PR adds proper-lockfile as a dependency and introduces two internal helpers (getUsersLocked, saveUsersLocked) in authController.js that serialize reads and writes to users.json using a file lock. The signup handler is reworked to acquire and release that lock explicitly, including in error paths. Integration tests covering success, duplicate, concurrent, weak-password, and missing-field cases are added along with matching test fixture data.

Changes

Concurrent-safe signup with file locking

Layer / File(s) Summary
Dependency and imports
package.json, src/controllers/authController.js
Adds proper-lockfile (^4.1.2) to dependencies and updates authController.js imports to bring in fs/promises and the lockfile module.
Locked helpers and signup handler rework
src/controllers/authController.js
Adds getUsersLocked (acquires lock, reads and parses JSON, returns users and release fn) and saveUsersLocked (writes via temp file, releases lock). Reworks signup to call these helpers for duplicate detection and persistence, and extends the catch block to release the lock on any exception before returning the 500 response.
Integration tests and test fixture data
server.test.js, src/data/users.json
Adds five POST /api/auth/signup integration tests: single success (201 + token), duplicate rejection (400 "User already exists"), concurrent registrations (all 201), weak password (400 matching strength pattern), and missing email/password fields (400). Updates users.json tail with test user records for those scenarios.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant signup as signup handler
  participant getUsersLocked
  participant saveUsersLocked
  participant users.json

  Client->>signup: POST /api/auth/signup (email, password)
  signup->>signup: validate fields & password strength
  signup->>getUsersLocked: acquire lock, read users.json
  getUsersLocked->>users.json: readFileSync + JSON.parse
  getUsersLocked-->>signup: users[], releaseFn
  alt email already exists
    signup->>getUsersLocked: releaseFn()
    signup-->>Client: 400 User already exists
  else new user
    signup->>signup: bcrypt.hash(password)
    signup->>saveUsersLocked: users.push(newUser), write + release
    saveUsersLocked->>users.json: write via tmp, rename
    saveUsersLocked->>getUsersLocked: releaseFn()
    signup-->>Client: 201 + JWT token
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #503 — Directly addresses the bug: unsynchronized read-modify-write operations on users.json during concurrent signups; this PR implements the file locking and concurrency tests prescribed in that issue.
  • #522 — PR implements the file-locking via proper-lockfile to atomically serialize signup writes, preventing the race condition described.
  • #469 — PR introduces the async fs/promises import and proper-lockfile-based locking improvements for concurrent signup serialization described in that issue.

Poem

🐇 Hop hop, no more race—
The lockfile holds a careful place,
Two bunnies sign up at once, you see,
But only one writes at a time, whee!
proper-lockfile guards the warren door,
No lost accounts upon the floor. 🔒

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: preventing concurrent signup race conditions and lost updates through file locking mechanisms.
Description check ✅ Passed The description includes all major template sections: Summary clearly explains the race condition fix, Changes Made details the implementation (file locking, atomic writes, error handling), and Verification Checklist confirms completion. Testing and Checklist sections are addressed within the description narrative.
Linked Issues check ✅ Passed All objectives from issue #503 are addressed: file locking via proper-lockfile prevents concurrent writes, duplicate validation moved inside critical section, atomic file writes implemented, and concurrent registration tests added covering parallel requests and lost-update prevention.
Out of Scope Changes check ✅ Passed All changes directly address the race condition issue: proper-lockfile dependency, file-locking helpers in authController, atomic write operations, lock cleanup, test coverage for concurrent signup, and users.json test data updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

@github-actions github-actions Bot added backend Express or API gateway work bug Something isn't working duplicate This issue or pull request already exists enhancement New feature or request feature A new feature or improvement fix A targeted fix or cleanup frontend Frontend-related work level:critical rag-service FastAPI / model service work type:security type:testing labels Jun 15, 2026

@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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/controllers/authController.js (1)

2-2: ⚡ Quick win

Remove unused symbols flagged by CI (fsPromises, saveUsers).

Line 2 and Lines 22-24 are now dead after the locked helper migration, and they keep lint warnings active.

Proposed cleanup
-const fsPromises = require("fs/promises");
@@
-const saveUsers = (users) => {
-  fs.writeFileSync(usersFile, JSON.stringify(users, null, 2));
-};

Also applies to: 22-24

🤖 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 `@src/controllers/authController.js` at line 2, Remove the unused fsPromises
import statement from line 2 and delete the saveUsers function definition from
lines 22-24. These symbols became dead code after the locked helper migration
and are causing lint warnings. Ensure no other code references these symbols
before deletion.

Sources: Linters/SAST tools, Pipeline failures

🤖 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.test.js`:
- Around line 988-1014: The test "POST /api/auth/signup - concurrent
registrations prevent lost updates" currently only verifies HTTP response status
codes and token presence, but does not verify that all three users were actually
persisted in the database. A lost-update bug could still pass this test by
returning 201 for all concurrent requests while failing to store some users.
After the Promise.all(promises) completes and you verify the successCount, add
post-signup verification by attempting to login each of the three users with
their respective email and password credentials. Verify that each login attempt
succeeds to confirm all three accounts were actually persisted, ensuring the
test truly catches lost-update bugs rather than just checking surface-level
response codes.
- Around line 946-1050: The signup tests (POST /api/auth/signup - single user
registration succeeds, POST /api/auth/signup - duplicate email is rejected, POST
/api/auth/signup - concurrent registrations prevent lost updates, POST
/api/auth/signup - validates password strength, POST /api/auth/signup - rejects
missing email or password) are writing directly to the real src/data/users.json
file and not cleaning up afterward, causing state to persist across test runs.
To fix this, implement test isolation by either mocking the user data store with
an in-memory implementation specific to each test, using a temporary test
fixture that gets restored after each test, or implementing setup/teardown hooks
to clear or reset the users.json file before and after each test runs. Ensure
that each test operates on isolated state that does not affect subsequent test
runs or the committed repository state.

In `@src/controllers/authController.js`:
- Around line 43-45: The empty catch block at line 45 that follows the
fs.unlinkSync(tempFile) call is violating the ESLint no-empty rule and is hiding
potential cleanup failures. Replace the empty catch block with proper error
handling by either logging the error that occurred during the file deletion
attempt (using a logger at an appropriate level like warn or debug since this is
cleanup code) or adding an explanatory comment if the error is intentionally
ignored. This will satisfy the linter and provide visibility into cleanup
failures for debugging purposes.

---

Nitpick comments:
In `@src/controllers/authController.js`:
- Line 2: Remove the unused fsPromises import statement from line 2 and delete
the saveUsers function definition from lines 22-24. These symbols became dead
code after the locked helper migration and are causing lint warnings. Ensure no
other code references these symbols before deletion.
🪄 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 Plus

Run ID: 6cadb328-991e-440d-a208-fe9e282f4a8e

📥 Commits

Reviewing files that changed from the base of the PR and between 5590b87 and e1380ac.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • package.json
  • server.test.js
  • src/controllers/authController.js
  • src/data/users.json

Comment thread server.test.js
Comment on lines +946 to +1050
test("POST /api/auth/signup - single user registration succeeds", async () => {
const testEmail = `test-single-${Date.now()}@example.com`;
const res = await fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: testEmail,
password: "Test@1234",
}),
});
assert.equal(res.status, 201);
const data = await res.json();
assert.ok(data.token);
assert.equal(data.message, "Signup successful");
});

test("POST /api/auth/signup - duplicate email is rejected", async () => {
const testEmail = `test-dup-${Date.now()}@example.com`;

const firstRes = await fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: testEmail,
password: "Test@1234",
}),
});
assert.equal(firstRes.status, 201);

const secondRes = await fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: testEmail,
password: "Different@1234",
}),
});
assert.equal(secondRes.status, 400);
const data = await secondRes.json();
assert.equal(data.message, "User already exists");
});

test("POST /api/auth/signup - concurrent registrations prevent lost updates", async () => {
const timestamp = Date.now();
const emails = [
`concurrent-1-${timestamp}@example.com`,
`concurrent-2-${timestamp}@example.com`,
`concurrent-3-${timestamp}@example.com`,
];

const promises = emails.map((email) =>
fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email,
password: "Test@1234",
}),
})
);

const results = await Promise.all(promises);

const successCount = results.filter((r) => r.status === 201).length;
assert.equal(successCount, 3, "All concurrent registrations should succeed");

const data = await results[0].json();
assert.ok(data.token);
});

test("POST /api/auth/signup - validates password strength", async () => {
const testEmail = `test-weak-${Date.now()}@example.com`;
const res = await fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: testEmail,
password: "weak",
}),
});
assert.equal(res.status, 400);
const data = await res.json();
assert.match(data.message, /Password must contain/);
});

test("POST /api/auth/signup - rejects missing email or password", async () => {
const res1 = await fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
password: "Test@1234",
}),
});
assert.equal(res1.status, 400);

const res2 = await fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "test@example.com",
}),
});
assert.equal(res2.status, 400);
});

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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Signup tests are non-hermetic and mutate the committed auth datastore.

These tests write directly to the real src/data/users.json and never restore it, which causes cross-run state bleed and fixture churn (already visible in this PR’s users.json delta).

One practical isolation pattern
+const fs = require("node:fs");
+const path = require("node:path");
+const usersFile = path.join(__dirname, "src/data/users.json");
+let usersSnapshot;
+
+before(() => {
+  usersSnapshot = fs.readFileSync(usersFile, "utf8");
+});
+
+after(() => {
+  fs.writeFileSync(usersFile, usersSnapshot, "utf8");
+});
🤖 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 `@server.test.js` around lines 946 - 1050, The signup tests (POST
/api/auth/signup - single user registration succeeds, POST /api/auth/signup -
duplicate email is rejected, POST /api/auth/signup - concurrent registrations
prevent lost updates, POST /api/auth/signup - validates password strength, POST
/api/auth/signup - rejects missing email or password) are writing directly to
the real src/data/users.json file and not cleaning up afterward, causing state
to persist across test runs. To fix this, implement test isolation by either
mocking the user data store with an in-memory implementation specific to each
test, using a temporary test fixture that gets restored after each test, or
implementing setup/teardown hooks to clear or reset the users.json file before
and after each test runs. Ensure that each test operates on isolated state that
does not affect subsequent test runs or the committed repository state.

Comment thread server.test.js
Comment on lines +988 to +1014
test("POST /api/auth/signup - concurrent registrations prevent lost updates", async () => {
const timestamp = Date.now();
const emails = [
`concurrent-1-${timestamp}@example.com`,
`concurrent-2-${timestamp}@example.com`,
`concurrent-3-${timestamp}@example.com`,
];

const promises = emails.map((email) =>
fetch(`${baseUrl}/api/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email,
password: "Test@1234",
}),
})
);

const results = await Promise.all(promises);

const successCount = results.filter((r) => r.status === 201).length;
assert.equal(successCount, 3, "All concurrent registrations should succeed");

const data = await results[0].json();
assert.ok(data.token);
});

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The concurrent-signup test does not verify persisted outcomes.

Right now it only checks response status/token. A lost-update bug can still return 201 for all requests while dropping one or more writes. Add post-signup verification (e.g., login each created user).

Strengthen the assertion
     const results = await Promise.all(promises);
 
     const successCount = results.filter((r) => r.status === 201).length;
     assert.equal(successCount, 3, "All concurrent registrations should succeed");
 
-    const data = await results[0].json();
-    assert.ok(data.token);
+    const loginResults = await Promise.all(
+      emails.map((email) =>
+        fetch(`${baseUrl}/api/auth/login`, {
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
+          body: JSON.stringify({ email, password: "Test@1234" }),
+        }),
+      ),
+    );
+    assert.equal(
+      loginResults.filter((r) => r.status === 200).length,
+      3,
+      "Each concurrently created user must be persisted and able to log in",
+    );
🤖 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 `@server.test.js` around lines 988 - 1014, The test "POST /api/auth/signup -
concurrent registrations prevent lost updates" currently only verifies HTTP
response status codes and token presence, but does not verify that all three
users were actually persisted in the database. A lost-update bug could still
pass this test by returning 201 for all concurrent requests while failing to
store some users. After the Promise.all(promises) completes and you verify the
successCount, add post-signup verification by attempting to login each of the
three users with their respective email and password credentials. Verify that
each login attempt succeeds to confirm all three accounts were actually
persisted, ensuring the test truly catches lost-update bugs rather than just
checking surface-level response codes.

Comment thread src/controllers/authController.js Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Express or API gateway work bug Something isn't working duplicate This issue or pull request already exists enhancement New feature or request feature A new feature or improvement fix A targeted fix or cleanup frontend Frontend-related work level:critical rag-service FastAPI / model service work type:security type:testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Concurrent User Registrations Can Cause Lost Updates Due to Unsynchronized users.json Writes

1 participant