11import { redirect } from "@remix-run/node" ;
2+ import { $replica } from "~/db.server" ;
23import { getUserById } from "~/models/user.server" ;
34import { sanitizeRedirectPath } from "~/utils" ;
45import { authenticator } from "./auth.server" ;
@@ -11,6 +12,44 @@ import {
1112} from "./sessionDuration.server" ;
1213import { getUserSession } from "./sessionStorage.server" ;
1314
15+ /**
16+ * Enforces the user's effective session duration (User.sessionDuration capped
17+ * by the most restrictive Organization.maxSessionDuration). If the session was
18+ * issued longer ago than the cap allows, throws a redirect to `/logout` and
19+ * emits a HIPAA audit log. `userId` is always the *session owner's* id (i.e.
20+ * the real authenticated user), not an impersonated one — because the cap
21+ * belongs to the cookie, not the impersonation target.
22+ */
23+ async function enforceSessionExpiry (
24+ request : Request ,
25+ userId : string ,
26+ impersonatedUserId : string | null = null
27+ ) : Promise < void > {
28+ const session = await getUserSession ( request ) ;
29+ // Hot path: every authenticated request runs this. Read from the replica
30+ // when one is configured (falls back to primary). Stale-by-replica-lag is
31+ // acceptable here because the worst case is a session living a few seconds
32+ // past its cap on the very first request after a cap change.
33+ const { durationSeconds, orgCapSeconds, userSettingSeconds } =
34+ await getEffectiveSessionDuration ( userId , $replica ) ;
35+ if ( ! isSessionExpired ( session , durationSeconds ) ) return ;
36+
37+ const issuedAt = getSessionIssuedAt ( session ) ;
38+ // HIPAA audit trail: structured log lands in CloudWatch via stdout. Use
39+ // the stable `event` field to filter/aggregate auto-logout events.
40+ logger . info ( "Auto-logout: session exceeded effective duration" , {
41+ event : "session.auto_logout" ,
42+ userId,
43+ impersonatedUserId,
44+ effectiveDurationSeconds : durationSeconds ,
45+ userSettingSeconds,
46+ orgCapSeconds,
47+ sessionAgeMs : issuedAt === null ? null : Date . now ( ) - issuedAt ,
48+ requestPath : new URL ( request . url ) . pathname ,
49+ } ) ;
50+ throw redirect ( "/logout" ) ;
51+ }
52+
1453export async function getUserId ( request : Request ) : Promise < string | undefined > {
1554 const impersonatedUserId = await getImpersonationId ( request ) ;
1655
@@ -20,39 +59,24 @@ export async function getUserId(request: Request): Promise<string | undefined> {
2059 if ( authUser ?. userId ) {
2160 const realUser = await getUserById ( authUser . userId ) ;
2261 if ( realUser ?. admin ) {
62+ // Enforce expiry against the admin's own session — impersonation must
63+ // not be a way to bypass the admin's effective duration cap.
64+ await enforceSessionExpiry ( request , authUser . userId , impersonatedUserId ) ;
2365 return impersonatedUserId ;
2466 }
2567 }
26- // Admin revoked or session invalid — fall through to return the real user's ID
68+ // Admin revoked or session invalid — fall through to return the real
69+ // user's ID. Same enforcement as the regular auth path below.
70+ if ( authUser ?. userId ) {
71+ await enforceSessionExpiry ( request , authUser . userId ) ;
72+ }
2773 return authUser ?. userId ;
2874 }
2975
3076 const authUser = await authenticator . isAuthenticated ( request ) ;
3177 if ( ! authUser ?. userId ) return undefined ;
3278
33- // Enforce the user's effective session duration (User.sessionDuration capped
34- // by the most restrictive Organization.maxSessionDuration). If the session
35- // was issued longer ago than the cap allows, force a logout.
36- const session = await getUserSession ( request ) ;
37- const { durationSeconds, orgCapSeconds, userSettingSeconds } = await getEffectiveSessionDuration (
38- authUser . userId
39- ) ;
40- if ( isSessionExpired ( session , durationSeconds ) ) {
41- const issuedAt = getSessionIssuedAt ( session ) ;
42- // HIPAA audit trail: structured log lands in CloudWatch via stdout. Use
43- // the stable `event` field to filter/aggregate auto-logout events.
44- logger . info ( "Auto-logout: session exceeded effective duration" , {
45- event : "session.auto_logout" ,
46- userId : authUser . userId ,
47- effectiveDurationSeconds : durationSeconds ,
48- userSettingSeconds,
49- orgCapSeconds,
50- sessionAgeMs : issuedAt === null ? null : Date . now ( ) - issuedAt ,
51- requestPath : new URL ( request . url ) . pathname ,
52- } ) ;
53- throw redirect ( "/logout" ) ;
54- }
55-
79+ await enforceSessionExpiry ( request , authUser . userId ) ;
5680 return authUser . userId ;
5781}
5882
0 commit comments