Skip to content

fix(server): session hardening — rotation, multi-device, isActive (#51) - #96

Open
Alimedhat000 wants to merge 1 commit into
mainfrom
feat/51-session-hardening-main
Open

fix(server): session hardening — rotation, multi-device, isActive (#51)#96
Alimedhat000 wants to merge 1 commit into
mainfrom
feat/51-session-hardening-main

Conversation

@Alimedhat000

@Alimedhat000 Alimedhat000 commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Closes #51

Summary

Hardens server session handling per #51 (3 issues) on a fresh branch off main (07ad659).

1. Refresh rotation + multi-device

  • New Session model (server/prisma/schema.prisma:26) — id, userId (FK CASCADE), refreshToken @unique, jti @unique, expiresAt + @@index([userId]) — migration 20260828010125_add_session. Replaces single User.refreshToken String? column that logged out other devices on each login.
  • login (server/src/controllers/auth.controller.ts:160) now mints jti = crypto.randomUUID(), signs refreshToken with jti, creates Session{ jti, expiresAt: +24h } per device; keeps legacy User.refreshToken dual-written for compat.
  • POST /auth/refresh (auth.controller.ts:337) — verifies JWT, looks up Session by jti, checks isActive/expiresAt, rotates to new jti/refreshToken and sets env-aware cookie (lax in dev/test, none; Secure in prod). Old token → 401 (REFRESH_TOKEN_REUSE), new token → 200. Legacy tokens without jti fall back to single-column check and are migrated to a Session on first refresh.
  • POST /auth/logout (auth.controller.ts:218) — deletes only presented Session (by jti or refreshToken) + legacy clear, with matching clearCookie attrs (Path=/, HttpOnly, SameSite, Secure).

2. isActive enforcement

  • login rejects !isActive with generic 401 (LOGIN_USER_INACTIVE) to avoid enumeration (Auth surfaces allow account enumeration (distinct errors for exists/not-found) #83).
  • refresh rejects deactivated before rotation (REFRESH_TOKEN_USER_INACTIVE).
  • authenticate middleware (server/src/middlewares/auth.middleware.ts:8) now async, verifies JWT then prisma.user.findUnique({isActive}) — deactivated JWTs rejected within 15m.

3. Cookie flags

Remaining / not in scope

  • Aggressive reuse-revoke-all on stolen-token detection currently just 401 (not deleteMany for that user) to avoid replay old → new token invalidated flake seen during rotation testing — can be added once migrated.
  • CSRF for SameSite=None prod cross-site refresh/logout — mitigated by Lax + 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.
  • Drop legacy User.refreshToken column — left dual-written for now.

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
Alimedhat000 force-pushed the feat/51-session-hardening-main branch from ce0d74c to 1d60087 Compare August 28, 2026 18:11
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.

Server session hardening: refresh rotation, isActive enforcement, cookie flags

1 participant