Skip to content

fix: correct password reset endpoint query logic#250

Merged
NickMonrad merged 1 commit into
mainfrom
fix/password-reset
Jun 6, 2026
Merged

fix: correct password reset endpoint query logic#250
NickMonrad merged 1 commit into
mainfrom
fix/password-reset

Conversation

@NickMonrad
Copy link
Copy Markdown
Owner

@NickMonrad NickMonrad commented Jun 3, 2026

Issue

Password reset was broken—users received 'Invalid or expired reset token' errors even with valid tokens.

Root Cause

  1. Initial bug: The endpoint tried to use prisma.passwordResetToken.update({ where: { tokenHash, usedAt: null } }) which fails silently because Prisma cannot filter on null in composite unique clauses.
  2. Race condition: After fixing the query, the usedAt === null check still happened outside the transaction. Two concurrent requests with the same token would both pass the guard, both enter the transaction, and both successfully reset the password (TOCTOU vulnerability).

Solution

  • Move the usedAt: null guard inside the transaction using updateMany (which supports arbitrary WHERE clauses, not just unique constraints)
  • Use updateMany to atomically mark token as used AND validate it hasn't been used yet
  • Only proceed to password update if count > 0 (i.e., token was successfully marked used by this request)
  • This ensures only the first concurrent request succeeds; duplicates are rejected before the password update

Changed Files

  • server/src/routes/auth.ts (lines 129–166)

Fixes #233

Code Review

This PR passed code review by Sonnet 4.6, which identified and approved the race condition fix. The updateMany pattern is the correct way to handle conditional atomic writes in Prisma when using non-unique WHERE clauses.

Testing

  • Schema validation tests pass (security.test.ts)
  • Ready for manual E2E test with UI or Playwright

The original code tried to use a compound where clause with usedAt: null
which Prisma cannot handle reliably. Changed to:
1. Find token by tokenHash alone
2. Verify usedAt is null explicitly
3. Use a transaction to atomically mark token as used and update password

This fixes the 'Invalid or expired reset token' error that was preventing
password resets from working.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NickMonrad NickMonrad merged commit 6adb791 into main Jun 6, 2026
1 check passed
@NickMonrad NickMonrad deleted the fix/password-reset branch June 6, 2026 11:18
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.

Resource Optimisation feature — find optimal resource counts and ramp-up dates for Timeline

1 participant