Repository navigation
feat(auth): session management: list and revoke active sessions per user - #143
Conversation
Adds a RefreshSession row per refresh-token family (one per login), with the refresh JWT carrying `sid`. Refresh is refused once the row is revoked, so revocation is enforced exactly where new tokens are minted. This is the revocation store issue DocForum#18 asked for, built in the auth module rather than as a parallel mechanism. Endpoints: GET /auth/sessions (marks the caller's current session), DELETE /auth/sessions/:id, DELETE /auth/sessions (sign out everywhere else), and admin DELETE /admin/users/:id/sessions mounted at /admin. Revoking the current session clears the refresh cookie in the same response; ownership is part of the revoking UPDATE's WHERE clause rather than a check above it, so another user's session id is simply a 404. lastUsedAt is written on refresh only, so authenticated requests don't add a write hot-spot. Refresh rotates tokens but keeps the same session family. Tokens minted before this change carry no sid and are rejected — the migration logs existing sessions out once, which is the honest trade for having a revocation store at all. Residual gap flagged, not hidden: access tokens stay stateless, so a revoked session's access token works for the rest of its 15-min TTL. Docs updated in the same change: ARCHITECTURE.md §3/§4.9/§6.1.4, ARCHITECTURE_ESSENTIALS.md, docs/api/README.md, ROADMAP.md. Refs DocForum#38
|
@Oyinkans0la12 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
CI is failing on this PR.
Please push a fix and CI will re-run. Before pushing, run |
|
CI fails at |
|
@Oyinkans0la12 Thanks for the PR. It can't be merged yet. To unblock it: CI is failing. Here's what broke:
Please fix these errors and run the same steps locally before pushing ( Merge conflicts: this branch conflicts with Push to this same branch and CI will re-run. Once it's green and conflict-free, it'll be reviewed for merge. |
|
@Oyinkans0la12 CI still fails at
Push to this branch and CI will re-run. Once it's green it can be merged. |
The main merge resolution dropped the RefreshSession model, the /auth sessions routes and admin router, the sid claim and session tracking in signup/login/refresh, leaving Prisma unable to generate and the session controllers calling services that were not exported. Also send the CSRF token on refresh in the sessions integration test, matching the current /auth/refresh contract.
afeezorobsco-cyber
left a comment
There was a problem hiding this comment.
Reviewed: no malicious code found, CI is green. Merging.
What changed
Implements session management from #38: a persisted
RefreshSessionrow perrefresh-token family (one per login), plus
GET /auth/sessions,DELETE /auth/sessions/:id,DELETE /auth/sessions(sign out everywhereelse), and admin
DELETE /admin/users/:id/sessions.Closes #38
Why
A patient who logged in on a shared clinic computer had no way to sign that
device out. Revocation in #18 gives the backend a kill switch; this gives users
and admins a way to use it, which is what makes revocation worth having.
Implementation notes
on [Phase 8] JWT refresh-token revocation #18's revocation store — [Phase 8] JWT refresh-token revocation #18 has not landed, so this PR is that store, in
the auth module where tokens are minted. The refresh JWT carries
sid; onPOST /auth/refreshtheRefreshSessionrow is looked up and a missing orrevoked session is refused with the same generic
401as any bad token. Thatis the moment revocation has to bite, because that's the operation that mints
new tokens.
"list my sessions" keeps meaning logins rather than growing a row per
refresh. Rotation still issues a brand-new refresh token.
lastUsedAtis written from refresh only (explicitlyrequired by the issue) — access-token-authenticated requests never touch the
table. Access tokens stay stateless, so no session lookup on any protected
route.
updateMany WHERE id = ? AND userId = ? AND revokedAt IS NULL; another user'ssession id updates zero rows and returns
404— no existence signal, and nobranch above the query to get wrong.
revoking a non-current one deliberately does not).
sidandare rejected, so the migration logs existing sessions out once. That's the
honest trade for actually having a revocation store rather than a decorative
table.
Schema change (proposed)
RefreshSession:id, userId, createdAt, lastUsedAt, revokedAt (nullable), userAgent (nullable), ip (nullable)— ARCHITECTURE.md §4.9.Migration:
backend/prisma/migrations/20261015182444_add_refresh_sessions/generated with
scripts/gen-migration.mjs(real Postgres diff).Coarse IP + user agent are stored because that's what makes "that other device"
recognizable; nothing beyond what the request already carried.
Assumptions / gaps flagged, not silently resolved
working for the remainder of its 15-minute TTL. Making that immediate needs a
per-request revocation check (or much shorter access tokens) and is a real
design decision, not a detail — recorded in §6.1.4 and
ARCHITECTURE_ESSENTIALS.md rather than quietly claimed as "instant".
POST /auth/logoutstill only clears the cookie; it doesn't revoke thesession. Changing that is
docforum-web's call to make alongside the UI, soit's flagged as a known gap instead of silently redefined here.
not at real scale).
Docs updated in the same PR
ARCHITECTURE.md(§3 module note, §4.9 model, §6.1.4 revocation gap nowaddressed with its residual note),
ARCHITECTURE_ESSENTIALS.md(data model mapdocs/api/README.md(all endpoints,sessionIdin thesession response, the refresh/
sidcontract),ROADMAP.md(Phase 8 item +changelog).
Tests
npm run typecheckandnpm run buildclean.npm run test→ 14 passed.npm run test:integration→ 9 passed (1 existing booking + 8 new).Integration (
tests/integration/sessions.test.mts, real Postgres, realcookies) covers: a session row per login; a user's list contains only their own
sessions and exactly one
current, tied to the request's cookie; refreshrotates tokens without forking the session and updates
lastUsedAt;one user cannot revoke (
404) or see another's session, and the target sessionsurvives; a revoked session's refresh is rejected immediately while the
caller's own keeps working; revoking the current session clears the cookie and
kills that refresh; "revoke all others" kills every other session and keeps the
current one; and admin revocation (403 for a non-admin, working for an admin,
with the target's refresh dead and their session list empty afterwards).
Cross-PR note
ARCHITECTURE.md§4,auth/services/tokens.tsandauth/services/index.tsare also touched by the open password-reset (#140) and2FA (#142) PRs — whichever lands second needs a trivial rebase and §4
subsection renumbering, called out rather than left as a surprise.