fix(server): session hardening — rotation, multi-device, isActive (#51) - #96
Open
Alimedhat000 wants to merge 1 commit into
Open
fix(server): session hardening — rotation, multi-device, isActive (#51)#96Alimedhat000 wants to merge 1 commit into
Alimedhat000 wants to merge 1 commit into
Conversation
Alimedhat000
added a commit
that referenced
this pull request
Aug 28, 2026
With Session rotation, POST /auth/refresh now invalidates the previous jti. The E2E suite reuses a single saved storageState (playwright/.auth) across sequential chromium tests — the first test's initial goto rotates the saved refresh token, so the second test's goto presents a stale jti and the bootstrap refresh 401s, leaving the dashboard hidden (17 failures in PR #96). Make createTestDocument resilient: after goto('/app'), if Dashboard heading not visible within 2s, re-login via UI and retry. Also update playwright.config.ts comment to reflect multi-device sessions + rotation. Keeps server rotation strict (old token → 401) while E2E self-heals.
- Add Session model (id, userId, jti, refreshToken, expiresAt) with migration 20260828010125_add_session; each login creates a Session instead of overwriting single User.refreshToken, enabling concurrent devices. - POST /auth/refresh now rotates jti/refreshToken, sets env-aware cookie (lax/non-secure in dev/test, none/secure in prod) and invalidates previous jti (401 on reuse). - POST /auth/logout deletes only the presented Session. - Enforce isActive in login, refresh and authenticate middleware. - Make E2E resilient to rotation by re-logging in when Dashboard not visible (shared storageState would otherwise present stale jti).
Alimedhat000
force-pushed
the
feat/51-session-hardening-main
branch
from
August 28, 2026 18:11
ce0d74c to
1d60087
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #51
Summary
Hardens server session handling per #51 (3 issues) on a fresh branch off
main(07ad659).1. Refresh rotation + multi-device
Sessionmodel (server/prisma/schema.prisma:26) —id, userId (FK CASCADE), refreshToken @unique, jti @unique, expiresAt+@@index([userId])— migration20260828010125_add_session. Replaces singleUser.refreshToken String?column that logged out other devices on eachlogin.login(server/src/controllers/auth.controller.ts:160) now mintsjti = crypto.randomUUID(), signsrefreshTokenwithjti, createsSession{ jti, expiresAt: +24h }per device; keeps legacyUser.refreshTokendual-written for compat.POST /auth/refresh(auth.controller.ts:337) — verifies JWT, looks upSessionbyjti, checksisActive/expiresAt, rotates to newjti/refreshTokenand sets env-aware cookie (laxin dev/test,none; Securein prod). Old token →401(REFRESH_TOKEN_REUSE), new token →200. Legacy tokens withoutjtifall back to single-column check and are migrated to aSessionon first refresh.POST /auth/logout(auth.controller.ts:218) — deletes only presentedSession(byjtiorrefreshToken) + legacy clear, with matchingclearCookieattrs (Path=/, HttpOnly, SameSite, Secure).2.
isActiveenforcementloginrejects!isActivewith generic401(LOGIN_USER_INACTIVE) to avoid enumeration (Auth surfaces allow account enumeration (distinct errors for exists/not-found) #83).refreshrejects deactivated before rotation (REFRESH_TOKEN_USER_INACTIVE).authenticatemiddleware (server/src/middlewares/auth.middleware.ts:8) nowasync, verifies JWT thenprisma.user.findUnique({isActive})— deactivated JWTs rejected within 15m.3. Cookie flags
isProduction ? 'none'+Secure : 'lax'aslogin/logout(added in fix(auth): secure refresh-token handling and rebuild client refresh logic #92).logoutclearing already mirrors attrs (from fix(auth): secure refresh-token handling and rebuild client refresh logic #92).Remaining / not in scope
401(notdeleteManyfor that user) to avoidreplay old → new token invalidatedflake seen during rotation testing — can be added once migrated.SameSite=Noneprod cross-site refresh/logout — mitigated byLax+ rate-limit + nginx same-origin proxy (nginx.conf does not proxy /api or /collaboration #38); add double-submit token if cross-site prod is required.User.refreshTokencolumn — left dual-written for now.