Login refuses a third concurrent password check and tells the user to wait 15 minutes #95

Closed
opened 2026-10-09 15:31:07 -04:00 by goddard · 1 comment
Collaborator

Login refuses any password check that arrives while two others are running, and tells the user to come back in 15 minutes. Anyone can trigger this from the public login endpoint.

  • passwords.js:18 rejects scrypt calls while two are active. Running three verifyPassword calls at once locally gives two results and one "Password service is busy".
  • http.ts:46-48 turns that into a 429 with Retry-After: 900.
  • index.ts:92 has already used up one of the user's rate-limit attempts before the check runs.

Unknown usernames still run scrypt, so an attacker with a few addresses can keep both slots busy and real users get refused. Three people signing in at the same moment also hit it. This is a separate path from the lockout in #63.

Fix

  • Queue password checks with a short timeout instead of rejecting immediately
  • Return a short Retry-After for "busy" instead of 900 seconds
Login refuses any password check that arrives while two others are running, and tells the user to come back in 15 minutes. Anyone can trigger this from the public login endpoint. - `passwords.js:18` rejects scrypt calls while two are active. Running three `verifyPassword` calls at once locally gives two results and one "Password service is busy". - `http.ts:46-48` turns that into a 429 with `Retry-After: 900`. - `index.ts:92` has already used up one of the user's rate-limit attempts before the check runs. Unknown usernames still run scrypt, so an attacker with a few addresses can keep both slots busy and real users get refused. Three people signing in at the same moment also hit it. This is a separate path from the lockout in #63. **Fix** - [x] Queue password checks with a short timeout instead of rejecting immediately - [x] Return a short `Retry-After` for "busy" instead of 900 seconds
goddard added the
priority
low
type/hardening
labels 2026-10-09 15:31:07 -04:00
josh was assigned by goddard 2026-10-09 15:31:07 -04:00
Author
Collaborator

Reviewed 4059fc30d6. Password checks now wait in a short queue instead of failing, and "busy" responses ask for a 2-second retry instead of 15 minutes, so I'm closing this.

Checklist

  • Queue password checks with a short timeout. passwords.js still runs at most two scrypt operations at once, but acquireSlot now queues up to 16 more for up to 2 seconds. releaseSlot hands a finished slot straight to the next waiter, so new requests can't jump the queue. A timed-out entry removes itself before rejecting, and derive releases its slot in finally, so failed hashes don't leak capacity. Five simultaneous verifyPassword calls that gave three "busy" errors before now all complete (about 1.4 s locally). auth-passwords.test.ts covers ordering, the 2-second expiry and the queue bound. auth-users.test.ts now shows three simultaneous logins succeeding with production scrypt.
  • Short Retry-After for "busy". authError (http.ts) now recognises busy errors by type (PasswordServiceBusyError) instead of message text and sends Retry-After: 2. The persistent rate limit still sends 900. The new test also checks that an unrelated error mentioning "busy" becomes a plain 500 with no Retry-After and no message leak.

Review notes

  • authentication.md now describes the queue size, the wait limit and both Retry-After values.
  • Tests: the 5 auth and launch test files pass locally (213 tests), the full suite at HEAD passes (209 files, 2,635 tests), and npm run check and npm run lint are clean. CI run 836 passed on this commit.
  • Minor and non-blocking: a request that ends up "busy" has still used one of the user's 10 attempts per 15 minutes (consumeAuthAttempt runs first, index.ts:92). With the queue in place that should be rare, so I don't think it needs changing.
Reviewed 4059fc30d659134208e0d5233b8bf29975df2402. Password checks now wait in a short queue instead of failing, and "busy" responses ask for a 2-second retry instead of 15 minutes, so I'm closing this. **Checklist** - [x] **Queue password checks with a short timeout.** `passwords.js` still runs at most two scrypt operations at once, but `acquireSlot` now queues up to 16 more for up to 2 seconds. `releaseSlot` hands a finished slot straight to the next waiter, so new requests can't jump the queue. A timed-out entry removes itself before rejecting, and `derive` releases its slot in `finally`, so failed hashes don't leak capacity. Five simultaneous `verifyPassword` calls that gave three "busy" errors before now all complete (about 1.4 s locally). `auth-passwords.test.ts` covers ordering, the 2-second expiry and the queue bound. `auth-users.test.ts` now shows three simultaneous logins succeeding with production scrypt. - [x] **Short `Retry-After` for "busy".** `authError` (`http.ts`) now recognises busy errors by type (`PasswordServiceBusyError`) instead of message text and sends `Retry-After: 2`. The persistent rate limit still sends 900. The new test also checks that an unrelated error mentioning "busy" becomes a plain 500 with no `Retry-After` and no message leak. **Review notes** - `authentication.md` now describes the queue size, the wait limit and both `Retry-After` values. - Tests: the 5 auth and launch test files pass locally (213 tests), the full suite at HEAD passes (209 files, 2,635 tests), and `npm run check` and `npm run lint` are clean. CI [run 836](https://gitea.thewrightserver.net/josh/electric-cherry/actions/runs/836) passed on this commit. - Minor and non-blocking: a request that ends up "busy" has still used one of the user's 10 attempts per 15 minutes (`consumeAuthAttempt` runs first, `index.ts:92`). With the queue in place that should be rare, so I don't think it needs changing.
Sign in to join this conversation.