fixed: refresh-token - #123
Open
Adeyemi-cmd wants to merge 1 commit into
Open
Conversation
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.
Close #119
PR: Session families, refresh-token replay detection, blocked-user enforcement & session cleanup
Summary
This PR hardens the refresh-token rotation flow and closes three security/hygiene gaps in the auth module:
auth.refresh_token_reuseaudit event is written, and re-authentication is forced.Response shapes are backward-compatible: success responses are unchanged, and legacy refresh tokens without a family claim keep returning the original
AUTH_SESSION_NOT_FOUNDerror.Changes
1. Session families + refresh-token replay detection
Files:
src/modules/auth/auth.service.ts,supabase/migrations/20260824000001_add_session_family_id.sql,src/modules/admin/audit.service.ts(consumed),src/modules/auth/auth.module.tssessions.family_id UUID NOT NULL DEFAULT gen_random_uuid()with indexidx_sessions_family_id.family_id. The refresh JWT now carries afamclaim ({ wallet, type: 'refresh', fam }).AUTH_SESSION_EXPIRED(unchanged).famclaim present → replay detected:AuditService: actionauth.refresh_token_reuse, resourcesession, metadata{ family_id }, after-state{ revoked_sessions: N }.401 AUTH_REFRESH_TOKEN_REUSEDand must sign in again.famclaim (pre-migration token) → falls back to the originalAUTH_SESSION_NOT_FOUNDresponse; nothing to revoke.AuthModuleimportsAdminModule(which exportsAuditService); no circular dependency introduced.2. Blocked-user enforcement on every request
Files:
src/modules/auth/user-status.service.ts(new),src/modules/auth/jwt.strategy.tsUserStatusService: per-wallet status lookup with an in-memory TTL cache.JwtStrategy.validate()is now async and callsensureNotBlocked(wallet)after signature verification. Blocked wallets get401 AUTH_USER_BLOCKEDon every request instead of retaining access until token expiry.USER_STATUS_CACHE_TTL_MS = 30_000). Blocking a wallet takes effect within ~30s on each instance, independent of the remaining access-token lifetime.Mapcache rather than Redis: the check runs on every request; a Redis round trip would double auth latency, and a 30s bound does not justify shared state. Multi-instance deployments each hold their own cache with the same bound.activeand the failure is logged — a DB blip must not lock out every authenticated user. Negative results (blocked) are also cached for the TTL, so repeated denied requests do not hammer the DB.invalidate(wallet)helper forces a fresh DB check (admin/test escape hatch).3. Session cleanup cron job
Files:
src/jobs/session-cleanup/session-cleanup.module.ts,src/jobs/session-cleanup/session-cleanup.service.ts(new),src/app.module.ts@Cron(CronExpression.EVERY_HOUR)mirroring the established nonce-cleanup pattern (per architecture rules:@nestjs/schedule, no BullMQ).expires_atolder than 1 hour (grace window mirrors nonce cleanup and keeps borderline "expired" responses accurate).app.module.tsalongside the other job modules.Files changed
supabase/migrations/20260824000001_add_session_family_id.sqlfamily_idcolumn + indexsrc/modules/auth/auth.service.tssrc/modules/auth/auth.module.tsAdminModule, provide/exportUserStatusServicesrc/modules/auth/jwt.strategy.tsvalidate(), blocked-user checksrc/modules/auth/user-status.service.tssrc/jobs/session-cleanup/*src/app.module.tsSessionCleanupModuletest/unit/modules/auth/auth.service.spec.tstest/unit/modules/auth/user-status.service.spec.tstest/unit/jobs/session-cleanup/session-cleanup.service.spec.tscontext/progress-tracker.md2026-08-24Tests
New coverage (13 tests):
refreshTokens(auth.service.spec.ts):famclaim preserved across rotation)auth.refresh_token_reuseaudit event written +AUTH_REFRESH_TOKEN_REUSEDfamclaim → originalAUTH_SESSION_NOT_FOUND, no audit eventAUTH_SESSION_EXPIREDAUTH_USER_BLOCKEDAUTH_REFRESH_TOKEN_INVALIDuser-status.service.spec.ts:
AUTH_USER_BLOCKEDwhen status is blockedinvalidate()forces fresh checksession-cleanup.service.spec.ts:
expires_atcutoff (cutoff asserted ≈ now − 1h)Also updated existing
generateTokenstests for thefamclaim and verified the session insert carries the samefamily_id.Verification
Acceptance criteria
audit_logsentry)Deployment notes
family_id; existing refresh tokens lack thefamclaim and degrade gracefully to the old behavior until their next login.Risks / trade-offs