Skip to content

feat(auth): centralize session-cache eviction on auth lifecycle (#48)#57

Merged
pranavp10 merged 1 commit into
mainfrom
auth-refactor-48-session-eviction
Jul 13, 2026
Merged

feat(auth): centralize session-cache eviction on auth lifecycle (#48)#57
pranavp10 merged 1 commit into
mainfrom
auth-refactor-48-session-eviction

Conversation

@pranavp10

@pranavp10 pranavp10 commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

  • Centralized session-validation cache eviction in the auth service (owner of lifecycle events).
  • Logout → delete that session-token cache entry (and prune user index).
  • Password-change / org-switch → read per-user index, delete all tokens + index.
  • Uses middleware-owned keys (session:token:…, session:user:…) + shared Redis prefix reloop-session.
  • Package tests drive each event and assert Redis entries are gone.

Stacked on #56 (#47). Closes #48.

Test plan

  • bun test in packages/auth — 33 pass (incl. 13 eviction cases)
  • Characterization tripwire still green
  • tsc --noEmit for @reloop/auth
  • CI green once stack base merges

Greptile Summary

This PR centralizes session-cache eviction in the auth lifecycle. The main changes are:

  • New eviction helpers for logout, password-change, and org-switch events.
  • Shared reloop-session Redis prefix for session validation cache keys.
  • Auth server after-hook wiring to trigger cache eviction.
  • Tests for the main eviction paths and key deletion behavior.

Confidence Score: 4/5

The session-cache eviction path needs fixes before merging.

  • Concurrent session validation can leave token cache entries without a user index.
  • Redis failures can let lifecycle requests finish while eviction did not happen.
  • Happy-path tests cover key deletion, but not the shared-index races or failure behavior.

packages/auth/src/middleware/eviction.ts; packages/auth/src/server/auth.ts

Security Review

The new eviction path can leave stale session-validation cache entries active when index updates race with session validation or Redis operations fail silently.

Important Files Changed

Filename Overview
packages/auth/src/middleware/eviction.ts Adds lifecycle eviction helpers, but the Redis index mutations are non-atomic and Redis failures can leave stale cache entries in place.
packages/auth/src/server/auth.ts Adds after-hook eviction wiring based on path, cookie, and session user context.
packages/auth/src/server/session-cache-redis.ts Adds the shared session-cache Redis instance using the expected prefix and constructor shape.
packages/auth/src/middleware/index.ts Re-exports the new eviction API from the middleware entrypoint.
packages/auth/src/server/index.ts Re-exports the shared session-cache Redis instance from the server entrypoint.
packages/auth/src/middleware/types.ts Documents the shared Redis prefix requirement for middleware config.
packages/auth/test/session-eviction.test.ts Adds happy-path coverage for lifecycle event mapping and cache key deletion.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
  participant Auth as Auth lifecycle hook
  participant Evict as eviction.ts
  participant Redis as Redis session cache
  participant MW as Session middleware

  Auth->>Evict: password-change/org-switch(userId)
  Evict->>Redis: get(session:user:userId)
  Redis-->>Evict: [oldToken]
  MW->>Redis: set(session:token:newToken)
  MW->>Redis: set(session:user:userId, [oldToken,newToken])
  Evict->>Redis: delete(session:token:oldToken)
  Evict->>Redis: delete(session:user:userId)
  Note over Redis: newToken cache entry survives while its index is removed
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
  participant Auth as Auth lifecycle hook
  participant Evict as eviction.ts
  participant Redis as Redis session cache
  participant MW as Session middleware

  Auth->>Evict: password-change/org-switch(userId)
  Evict->>Redis: get(session:user:userId)
  Redis-->>Evict: [oldToken]
  MW->>Redis: set(session:token:newToken)
  MW->>Redis: set(session:user:userId, [oldToken,newToken])
  Evict->>Redis: delete(session:token:oldToken)
  Evict->>Redis: delete(session:user:userId)
  Note over Redis: newToken cache entry survives while its index is removed
Loading

Reviews (1): Last reviewed commit: "feat(auth): centralize session-cache evi..." | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.

Make session validation cache revocation near-instant. Auth service hooks
logout, password-change, and organization-switch to evict using the shared
middleware key convention (session:token / session:user).

- applySessionCacheEviction + path→event mapping in @reloop/auth/middleware
- sessionCacheRedis with SESSION_CACHE_REDIS_PREFIX for cross-service keys
- Wire Better Auth after-hooks in the runtime instance
- Package tests assert token + user-index entries are gone after each event
Comment on lines +64 to +68
const tokens = (await redis.get<string[]>(indexKey)) ?? [];
for (const token of tokens) {
await redis.delete(sessionTokenCacheKey(token));
}
await redis.delete(indexKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Bulk Eviction Drops New Sessions

When password-change or org-switch eviction runs while another request caches a session for the same user, this function only deletes the tokens read before the race and then deletes the whole user index. The newly cached session:token:* entry can survive while its index entry is removed, so later bulk evictions cannot find it and stale session state can continue to be accepted until that cache entry expires.

Comment on lines +49 to +54
const tokens = (await redis.get<string[]>(indexKey)) ?? [];
const next = tokens.filter((t) => t !== sessionToken);
if (next.length === 0) {
await redis.delete(indexKey);
} else {
await redis.set(indexKey, next);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Logout Overwrites Concurrent Index Writes

This read/filter/write of session:user:* can race with the middleware path that appends a token to the same index. A logout can write an older filtered array after a concurrent session validation added a live token, leaving that token's cache entry present but no longer listed for future password-change or org-switch eviction.

Comment on lines +119 to +123
try {
await applySessionCacheEviction(redis, event);
} catch {
// ignore — auth path must succeed even if cache is down
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Redis Eviction Fails Open

The Redis cache client used here resolves get, set, and delete failures instead of throwing, so this catch block usually never sees a Redis outage or failover. Logout, password-change, and org-switch requests can then return normally while no cache entries were deleted, leaving stale session-validation results active for their remaining TTL with no retry or error signal.

Comment on lines +139 to +147
const sessionUser =
// biome-ignore lint/suspicious/noExplicitAny: Better Auth context shape
(context as any)?.session?.user ??
// biome-ignore lint/suspicious/noExplicitAny: Better Auth context shape
(context as any)?.newSession?.user;
await handleAuthLifecycleEviction(sessionCacheRedis, {
path: String(path),
cookieHeader,
userId: sessionUser?.id ?? null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Sign-Out Can Lose User Index Pruning

For /sign-out, the after-hook may run after Better Auth has cleared context.session, which makes userId fall back to null. The logout event still deletes the token from the cookie, but evictSessionByToken skips pruning session:user:*, leaving dead tokens in the user index and causing later user-wide evictions to operate on stale index contents.

Base automatically changed from auth-refactor-47-middleware-plugin to main July 13, 2026 05:13
@pranavp10 pranavp10 merged commit 23b8e74 into main Jul 13, 2026
1 check passed
@pranavp10 pranavp10 deleted the auth-refactor-48-session-eviction branch July 13, 2026 05:13
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.

Auth refactor 5/10 — Centralized session-cache eviction in the auth service

1 participant