closes the ttl-policy + revocation pieces left from #110.
absolute-timeout middleware:
every request that survives express-session runs through a new
middleware that checks req.session.absoluteExpiresAt. past the
12 h hard ceiling, it destroys the redis-side session, clears the
portal_session cookie, drops the entry from the per-user index,
and lets the request keep flowing anonymously. route-level
guards (/me today, future @requireauth) turn that into a 401
where auth is actually needed — public routes stay accessible.
not the "every route returns 401" reading of adr-0010 §"ttl
policy" (validated with the project lead): "returns 401" is
interpreted as "the user eventually sees a 401 when they touch
something that needs auth", not as a hard refusal on the
current in-flight request.
user_sessions:{userId} secondary index (adr-0010 §"revocation"):
a new UserSessionIndexService maintains a redis set of active
session ids per user. wired on /auth/callback (sadd at sign-in,
after session.save() so the session row exists before the index
points at it) and on /auth/logout + the absolute-timeout
middleware (srem on destroy). best-effort: a failed sadd/srem
logs a pino warning and the auth flow continues — the index is
a convenience for admin operations, not a security invariant.
orphans (entries whose session:<id> redis key has expired on
idle ttl) are tolerated per the adr; future consumer code
filters them out.
no in-product consumer ships in this pr — the admin "logout
everywhere" endpoint lands with the admin module (+ @requireadmin
/ @requiremfa guards). today the index is write-only on the bff
side.
session payload extension:
createdAt and absoluteExpiresAt are now set on the session at
the same moment as req.session.user, in /auth/callback. the
session.types.ts declaration merging exposes them as optional
SessionData fields so every consumer sees a typed payload.
wiring:
- SessionModule provides UserSessionIndexService and a
SESSION_ABSOLUTE_TIMEOUT_MIDDLEWARE token whose factory builds
the middleware via createAbsoluteTimeoutMiddleware(index,
logger). same pattern as the existing SESSION_MIDDLEWARE token.
- AuthModule now imports SessionModule so AuthController can
inject UserSessionIndexService.
- main.ts mounts the absolute-timeout middleware immediately
after the session middleware.
per-user identifier is the entra `oid` claim (stable per-user
inside the tenant, matches req.session.user.oid). future
multi-tenant scenarios may want `${tid}:${oid}` — easy refactor
when external id activation lands (adr-0008). not in scope here.
out of scope, landing in follow-ups:
- admin "logout everywhere" endpoint consuming
UserSessionIndexService.list(userId)
- audit-pipeline first-class events for session.absolute_timeout
+ user_session_index.* (adr-0013)
- token blob persistence (id_token / access_token / refresh_token)
in the encrypted session — adr-0014 dependency
123/123 tests pass (was 110); +13 specs across the two new files
and the auth controller spec.