feat(auth): Sprint 10 — session self-service (list + revoke)#5
Merged
Conversation
SPEC FR-012: authenticated users can list their active sessions and
revoke any of them, plus a "log out other devices" convenience.
## What's new
Three user-facing endpoints, guarded by `JwtAuthGuard`:
```
GET /me/sessions → SessionDto[] — list, newest-first, with
`current: boolean` marker for this device
DELETE /me/sessions/:jti → 204 revoke one; 404 if jti isn't yours
POST /me/sessions/logout-others → revoke all EXCEPT the caller's current session
```
## TokenService additions
- `listSessionsForUser(userId)` — iterates `session:user:{userId}` set,
fetches each session metadata, filters expired, sorts newest-first.
Opportunistic cleanup: stale jtis (Redis key expired but set still
has the member) are pruned in the background.
- `revokeSessionForUser(userId, jti)` — revokes + removes from set,
BUT only if the jti is actually a member of the caller's set. A
stolen jti alone can't revoke another user's session.
- `revokeAllSessionsExcept(userId, keepJti)` — "logout other devices"
primitive. Never touches the preserved jti; never touches sessions
that aren't in the caller's set.
## Security shape
- Membership check before revoke: the SREM check against
`session:user:{userId}` means a malicious client that guesses a jti
belonging to someone else gets `false`, not a revocation.
- 404 (not 403) on unknown/foreign jti: can't probe for others'
session ids from the response shape.
- Self-only: all three endpoints use `@CurrentUser` for the userId —
no path parameter that could be tampered with.
## Tests (10 new, 312 total)
`apps/core/test/session.integration.spec.ts`:
- listSessionsForUser: empty, multiple entries w/ correct timestamps,
cross-user isolation, stale-jti opportunistic cleanup
- revokeSessionForUser: own session revokes + verify-fails afterwards,
cross-user attempt returns false, unknown jti returns false
- revokeAllSessionsExcept: keeps current + revokes others, no-op when
only current, cross-user isolation
Registration issues a session as a side effect; tests clear it via
`revokeAllSessionsForUser` right after `makeAuthor` so each test
starts from a known zero-session baseline.
## Verification
- `npx tsc --noEmit` clean
- `npx biome check .` clean (139 files)
- `npx vitest run` **312/312** (302 + 10 Sprint 10)
- `npm run build:core` + `build:crdt` both clean
## Not in scope (future sprints)
- Device/browser fingerprinting in session metadata (would need client
cooperation + schema change) — Sprint 11+
- 2FA (FR-013) — Sprint 11
- Audit log for session operations (currently only info-logged) —
folds into general AuditLog from Sprint 11
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
SPEC FR-012: authenticated users can now list their active sessions, revoke any of them, and "log out other devices" — the full session self-service surface.
All Redis infrastructure already existed (Sprint 1 TokenService); this PR adds the three service methods + one controller to expose it.
Endpoints
TokenService additions
listSessionsForUser(userId)— iterates per-user set, fetches metadata, newest-first. Opportunistic SREM of stale jtis (key expired but set still has them) keeps the list fast long-term.revokeSessionForUser(userId, jti)— SREM membership check first — a stolen jti can't revoke someone else's session because the set query uses the authenticated userId, not the path param.revokeAllSessionsExcept(userId, keepJti)— preserves the caller's current session so they don't log themselves out mid-click.Security shape
@CurrentUserfor userId; no path param can impersonate.Tests (10 new, 312 total)
apps/core/test/session.integration.spec.ts:Registration issues a session as a side effect (login-on-register); tests clear it post-registration so each assertion reasons about an empty baseline.
Verification
npx tsc --noEmitcleannpx biome check .clean (139 files)npx vitest run312/312 (302 + 10 Sprint 10)npm run build:core+build:crdtboth cleanNot in scope
🤖 Generated with Claude Code