Bug Description
refreshToken (backend/controllers/authController.js) validates the incoming refresh token against the current stored hash, then unconditionally overwrites user.refreshTokenHash with a newly rotated hash. There is no optimistic concurrency check (no compare-and-swap) tying the write to the specific old hash value that was validated, so concurrent refresh calls using the same token interfere with each other.
Steps to Reproduce
- Log in and obtain a valid refreshToken.
- Fire two concurrent POST requests to /api/auth/refresh using the same refreshToken (e.g. simulating multiple tabs or a double-fired axios interceptor).
- Attempt to use both returned refreshTokens.
Expected Behavior
Concurrent legitimate refresh attempts using the same valid token should be handled gracefully (e.g. one succeeds cleanly, or both receive a consistent, valid rotated token), without forcing an unrelated re-login, and any true token-reuse-after-rotation should be distinguishable and trigger revocation.
Actual Behavior
Both requests pass bcrypt.compare against the same pre-rotation hash, then both rewrite refreshTokenHash — whichever save() lands last wins, silently invalidating the other request's newly issued refresh token. The losing client is forced into a full re-login with no distinct error from an actual stolen-token scenario.
Severity
Critical
Screenshots / Screen Recording
No response
Browser
No response
Operating System
No response
Additional Context
No response
Bug Description
refreshToken(backend/controllers/authController.js) validates the incoming refresh token against the current stored hash, then unconditionally overwrites user.refreshTokenHash with a newly rotated hash. There is no optimistic concurrency check (no compare-and-swap) tying the write to the specific old hash value that was validated, so concurrent refresh calls using the same token interfere with each other.Steps to Reproduce
Expected Behavior
Concurrent legitimate refresh attempts using the same valid token should be handled gracefully (e.g. one succeeds cleanly, or both receive a consistent, valid rotated token), without forcing an unrelated re-login, and any true token-reuse-after-rotation should be distinguishable and trigger revocation.
Actual Behavior
Both requests pass bcrypt.compare against the same pre-rotation hash, then both rewrite refreshTokenHash — whichever save() lands last wins, silently invalidating the other request's newly issued refresh token. The losing client is forced into a full re-login with no distinct error from an actual stolen-token scenario.
Severity
Critical
Screenshots / Screen Recording
No response
Browser
No response
Operating System
No response
Additional Context
No response