fix(security): rate limit register endpoint; remove RESIDENT_TOKEN cookie#467
fix(security): rate limit register endpoint; remove RESIDENT_TOKEN cookie#467conorluddy wants to merge 2 commits into
Conversation
9ab2fab to
a7a1c8d
Compare
0fe8e10 to
f100368
Compare
Code Review β PR #467: rate limit register endpoint; remove RESIDENT_TOKEN cookieGood security improvements overall. A few things worth discussing. β What's goodRate limiting Applying
Exposing the raw DB user ID on every auth response was unnecessary β it's already embedded in the JWT. Removing it reduces the attack surface. The change to derive π‘ Issues / Suggestions1. const token = await SERVICES.getToken({ tokenId: refreshTokenId })
if (!token?.userId) {
throw new BadRequestError(MESSAGES.MISSING_USER_ID)
}
2. If a user's refresh token has already expired and been cleaned from the DB, // Always clear the cookie
res.cookie(REFRESH_TOKEN, '', { httpOnly: true, ..., expires: new Date(0) })
// Best-effort DB cleanup
const token = await SERVICES.getToken({ tokenId: refreshTokenId })
if (token?.userId) {
await SERVICES.deleteRefreshTokensByUserId({ userId: token.userId })
}
handleSuccessResponse({ res, message: MESSAGES.LOGOUT_SUCCESS })3. The "destroy on detection" pattern (delete all tokens before checking validity) is carried forward, which is intentional for detecting token theft. Just flagging it explicitly: a replayed or malformed refresh token will wipe all sessions for that user. This is a known trade-off β worth a code comment if not already present. 4. The happy path is covered via the mocked 5. Removed security tests β appropriate The removed tests ( π΄ BlockerIssue #2 (logout regression) is worth addressing before merge β a user who tries to log out after their refresh token has expired will get an error instead of a clean logout. At minimum this should fail silently rather than 400. |
Code Review β PR #467: rate limit register; remove RESIDENT_TOKEN cookieSummaryGood follow-up to #466. Both changes are well-motivated. A few things worth considering. β
Rate-limit on
|
Code Review β PR #467: rate limit register endpoint; remove RESIDENT_TOKEN cookieOverallThe rate-limiting addition is straightforward and correct. The
|
| Area | Status |
|---|---|
Rate limit on /register |
β Correct |
| RESIDENT_TOKEN removal | β Good cleanup |
| refreshToken userId source | β DB is authoritative |
| Session wipe on invalid token | |
| Logout error message | |
| Test coverage | β Updated correctly |
Verdict: Approve pending the error message fix in logout.ts.
π€ Reviewed with Claude Code
Code Review β PR #467: rate limit register endpoint; remove RESIDENT_TOKEN cookieGood security improvements. Two items worth discussing.
|
Code Review β PR #467Overall: Approve with one functional concern to discuss. The rate-limit fix and cookie cleanup are both straightforward improvements. One edge case in the new logout flow is worth examining.
|
Code Review - PR 467: rate limit register endpoint; remove RESIDENT_TOKEN cookieThis PR correctly addresses two distinct high-severity issues. Review below. StrengthsRate limiting on /register: Applying rateLimitTenPerTenMins to POST /users/register is the right call. The loose global limiter (100/10 min) was trivially bypassable for account enumeration and bulk registration attacks. Matching the same limit used by login, magic-login, and reset-password gives consistent protection. RESIDENT_TOKEN removal: Eliminating this cookie removes a low-value information leak (the internal DB user ID) with no loss of functionality since the ID is already inside the JWT. The refactors to resolve userId from the DB token record in refreshToken.ts and logout.ts are the correct approach. refreshToken.ts - userId is now DB-authoritative: Sourcing userId from the stored token record rather than from a client-supplied cookie is strictly more secure. The cross-validation check (token.userId !== userId) was only meaningful when userId came from a cookie; removing it is correct. Token invalidation order: deleteRefreshTokensByUserId is now correctly guarded inside if (token), avoiding a null-dereference on the pre-existing path. Invalidating all tokens for a user when any refresh token is presented (valid or not) is intentional token-theft detection behavior and is preserved. Issues / Noteslogout.ts - wrong error message for token-not-found case When SERVICES.getToken returns null, the code throws BadRequestError(MESSAGES.MISSING_USER_ID). But the actual problem is that the refresh token ID was not found in the database - MESSAGES.TOKEN_NOT_FOUND would be more accurate and easier to debug. MISSING_USER_ID suggests a malformed request rather than an unknown token. logout.ts - existing residentToken cookies not cleared Clients that authenticated before this change will still carry the residentToken cookie until it expires. The logout handler no longer clears it. This is a minor issue since the cookie carries no server-side authority, but adding a cookie clear with expires: new Date(0) in the logout response would clean it up for existing sessions without any downside. refreshToken.ts - deleteRefreshTokensByUserId before validity checks If a refresh token is used or expired, all tokens for that user are still deleted before the error is thrown. This is intentional (token reuse = likely theft, so nuke all sessions) but it means a leaked or replayed old token can log out a legitimate user. This is pre-existing behavior that the PR does not worsen; just worth documenting the trade-off. routes/auth/logout.test.ts - still skipped The route-level logout test remains in describe.skip. Given the significant logic changes in this PR (DB lookup added, cookie dependency removed), it would be valuable to have this test running. VerdictBoth fixes are correct and improve the security posture. The TOKEN_NOT_FOUND message issue in logout.ts is worth a quick fix. Everything else is minor. |
Code Review: PR 467 - rate limit register; remove RESIDENT_TOKEN cookieHigh: Register rate limitingApplying rateLimitTenPerTenMins directly on the route is consistent with how login, magic-login, and reset-password are configured. Good. High: RESIDENT_TOKEN removalRemoving the redundant user-ID cookie simplifies the auth cookie surface area. The ID is already carried in the JWT payload, so this was dead data being serialised on every auth response. logout.ts - minor concernThe error thrown when the refresh token cookie is absent uses MESSAGES.MISSING_USER_ID. At that point no DB lookup has happened - the missing thing is actually the refresh token cookie, not a user ID. MESSAGES.REFRESH_TOKEN_REQUIRED (already defined) would be more accurate and easier to trace in logs. Also consider adding a test for the new missing-refresh-token path: refreshToken.ts - quiet but meaningful DoS improvementThe old code called deleteRefreshTokensByUserId unconditionally using the cookie value. An attacker who guessed a valid user ID could submit random refresh token IDs to force-logout arbitrary users. The new code only deletes tokens when the submitted ID actually exists in the DB, which closes that vector. Well spotted. Removed test coverage
LGTM on the overall approach. One action item: update the error message on the missing-refresh-token path in logout.ts. |
Code ReviewSummaryBoth changes are good security improvements. One logic simplification is worth addressing before merging.
|
β¦okie Register endpoint now uses rateLimitTenPerTenMins (10/10min) matching login, magic-login, and reset-password β previously only had the loose 100-req/10-min global limiter. RESIDENT_TOKEN cookie exposed the user's internal ID unnecessarily on every login/refresh response. Removed from all auth flows (login, refreshToken, magicLoginWithToken, googleCallback). logout and refreshToken now resolve userId via DB token lookup instead of trusting a separate cookie. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
β¦alidate comment logout: degrade gracefully when refresh token not in DB (expired/purged) β cookie is already cleared so user is effectively logged out regardless. Fix misleading MISSING_USER_ID β REFRESH_TOKEN_REQUIRED error message. Add test for token-not-found path. refreshToken: add explanatory comment on the intentional delete-before-validate ordering (token theft detection: nuke all sessions on any suspicious reuse). Use guard clause to remove redundant if(token)/if(!token) double-check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
f100368 to
7391152
Compare
Code Review:
|
Code Review: PR 467 - rate limit register; remove RESIDENT_TOKEN cookieOverall: Approve with one design noteWhat is goodRate limiting on POST /users/register β the endpoint was only protected by the loose global limiter (100 req/10 min). Moving it to rateLimitTenPerTenMins (10/10 min) matches the protection already in place on login, magic-login, and reset-password. Correct fix. RESIDENT_TOKEN cookie removal β exposing the raw DB user ID on every auth response with no server-side benefit was unnecessary. The ID is already inside the signed JWT, so removing this extra cookie is a clean security improvement. Logout refactoring β resolving userId from the DB token record instead of trusting a separate cookie is the right approach. The graceful-degradation test (succeeds even when token is not in DB) is a good addition. refreshToken refactoring β userId is now authoritative from the DB record, not from a client-supplied cookie. This is strictly more secure. Design note: session-nuke order in refreshToken.tsdeleteRefreshTokensByUserId is called before token.used is checked: The intent (token reuse = possible theft = nuke all sessions) is documented in the code comment, and the design is a valid security tradeoff. However, this means a legitimate user experiencing a network retry on a just-used token silently loses all their other active sessions. Worth considering whether a dedicated test covers this scenario and whether the error surfaced to the client gives enough signal that they need to re-authenticate everywhere. Issues to fix before merging1. Dead message constants MESSAGES.TOKEN_USER_INVALID and MESSAGES.REFRESH_TOKEN_COUNTERPART_REQUIRED are now unreferenced β the checks that used them were removed in this PR and PR 466 respectively. Remove them here or in PR 466 to avoid dead code. 2. Skipped logout.test.ts scenario for missing refresh token The test description was updated from 'Throws an error if missing the user data' to 'Throws an error if the refresh token cookie is absent', which correctly reflects the new logic. Good. One scenario worth adding: what happens when req.cookies is present but the refreshToken key is an empty string vs undefined β both should be handled by the existing guard, but a test would confirm. |
Code ReviewGood security hardening - the rate limit fix is straightforward and the RESIDENT_TOKEN removal is the right call. A few things worth discussing. What is well doneRate limiting on /register - Correctly applies rateLimitTenPerTenMins (matching login, magic-login, reset-password), closing an obvious enumeration/spam vector. Placement before VALIDATE.email in the middleware chain is correct. RESIDENT_TOKEN cookie removal - The user ID was already encoded in the JWT; exposing it as a separate cookie added surface area with no benefit. Good removal. Logout via refresh token DB lookup - logout.ts now uses SERVICES.getToken to resolve userId from the refresh token rather than trusting a separate cookie. This is strictly more secure and the graceful-degradation path (token already expired/purged) is well-handled and tested. RefreshToken: userId from DB - token.userId is now the authoritative source rather than a separate cookie value. The old cross-validation check (token.userId !== userId) becomes redundant once both sides come from the same DB record, so removing it is correct. IssuesRedundant deleteToken call in refreshToken.ts (line 69) The sequence is:
Step 4 is a no-op since the token was already removed in step 2. This adds an unnecessary DB round-trip on every successful token refresh. The deleteToken call should be removed. Aggressive session invalidation trade-off (refreshToken.ts lines 32-39) The comment documents the intent well, but the current order means a legitimate expired token triggers deletion of all active sessions:
For an already-expired token that has not been rotated (e.g., user was simply inactive), this nukes all their other active devices. This is the documented trade-off but it is worth confirming this is the desired UX. An alternative is to validate .used and expiresAt before deleting other sessions, and only nuke-all on .used (which indicates token reuse/theft), not on natural expiry. Minor notes
Test coverageNew tests added in logout.test.ts are good - graceful degradation on missing DB token is an important case to cover. The refreshToken.test.ts mock cleanup is clean. Verdict: Mostly approve. Remove the redundant deleteToken call (step 4 above). Consider the expiry-vs-reuse distinction in session invalidation if multi-device support matters. |
Code Review β PR #467: rate limit register endpoint; remove RESIDENT_TOKEN cookieStacks cleanly on #466. Both changes are good security improvements. A couple of things worth discussing. β Rate limit on /registerSimple and correct. POST /register is now on rateLimitTenPerTenMins, matching login, magic-login, and reset-password. Without this, an attacker could enumerate or spam registrations at 100 req/10 min. β Removal of RESIDENT_TOKEN cookieRemoving the cookie that exposed the internal DB user ID on every auth response is the right call β it was redundant (the ID is in the JWT payload) and unnecessarily leaked an internal identifier.
|
Code ReviewStrengthsRate-limiting the register endpoint: Correct fix. Without this, account-creation brute force / enumeration was uncapped. Matching the 10/10-min rate used by login and magic-login is the right choice. RESIDENT_TOKEN removal: Clean and correct. The userId was redundant (available in the JWT payload); the cookie exposed the internal DB ID to the browser with no server-side benefit. Logout via refresh-token DB lookup: The new pattern (look up userId from the DB record rather than a separate cookie) is more secure β it removes a client-supplied value from the trust chain. The graceful-degradation path (cookie already cleared even if the DB lookup returns null) is a good UX decision and is now tested. refreshToken β userId now authoritative from DB: Removing the RESIDENT_TOKEN cross-check is valid now that userId is derived solely from the DB token record. No tamper vector remains at this layer. IssuesAggressive session invalidation on any invalid token (refreshToken.ts): deleteRefreshTokensByUserId is called after finding the token in the DB but before validating whether it is used or expired. This means a legitimately expired token (e.g. user waited too long before refreshing) will silently nuke all of that user's active sessions. The comment acknowledges the trade-off, but the UX impact is significant β consider logging a warning when this path is taken so the behaviour is observable, or limiting the nuke to the token-reuse (used === true) case only and returning a 401 for the expiry case. Missing test coverage for the session-nuke path: The deleted test ('the token user does not match the JWT user') tested cross-user validation, which is now gone. There is no new test asserting that presenting a used or expired token triggers deleteRefreshTokensByUserId. This is an important security behaviour that should be tested. Non-blocking observations
|
Code Review: fix(security): rate limit register endpoint; remove RESIDENT_TOKEN cookieSolid direction overall. One security trade-off in Strengths
Issues1. All sessions are nuked before the token is fully validated (medium) In
The comment says this is intentional ("token theft" signal), but the trade-off is a meaningful DoS vector: any attacker who obtains a stale (but still DB-present) token ID can present it and wipe all active sessions for that user, even if the subsequent checks would reject it. The token is deleted on a normal flow ( Consider moving the broad session nuke to after if (token.used) {
// Detected reuse β THIS is the clear signal for token theft
await SERVICES.deleteRefreshTokensByUserId({ userId: token.userId })
throw new ForbiddenError(MESSAGES.TOKEN_USED)
}2. Removed test: "the token user does not match the JWT user" (low) This check was removed because 3. The new test covers Notes
|
## Summary > Stacks on #467 β merge that first. - **Medium:** `jwt.sign` and `jwt.verify` now explicitly specify `algorithm: 'HS256'` / `algorithms: ['HS256']`. Prevents algorithm confusion attacks (e.g. `alg: none`) if the library ever changes its default or an attacker crafts a malicious token. - **Medium:** `express.json()` now has an explicit `limit: '10kb'` β makes the body size cap intentional and documented rather than relying on the library default. ## Test plan - [ ] `npm run lint && npm test` green - [ ] Manual: attempt a JWT with `alg: none` β verify rejected - [ ] Manual: send a JSON body >10kb β verify 413 response π€ Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
POST /users/registerwas only protected by the loose global rate limiter (100 req/10 min). Now usesrateLimitTenPerTenMins(10/10 min), matching login, magic-login, and reset-password.RESIDENT_TOKENcookie was exposing the user's internal DB ID on every auth response with no server-side benefit (the ID is already in the JWT). Removed from all auth flows.logoutandrefreshTokennow resolveuserIdfrom the DB token record instead of trusting a separate cookie.Test plan
npm run lint && npm testgreenPOST /users/register11 times from same IP in 10 min β verify 429 on the 11thresidentTokencookieπ€ Generated with Claude Code