Skip to content

Commit 2726647

Browse files
committed
Code review fixes and improvements
1 parent 8062e16 commit 2726647

7 files changed

Lines changed: 62 additions & 35 deletions

File tree

apps/webapp/app/routes/account.security/route.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "~/components/layout/AppLayout";
99
import { Header2 } from "~/components/primitives/Headers";
1010
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
11+
import { $replica } from "~/db.server";
1112
import { requireUser } from "~/services/session.server";
1213
import {
1314
getAllowedSessionOptions,
@@ -27,7 +28,7 @@ export const meta: MetaFunction = () => {
2728
export async function loader({ request }: LoaderFunctionArgs) {
2829
const user = await requireUser(request);
2930

30-
const { durationSeconds, orgCapSeconds } = await getEffectiveSessionDuration(user.id);
31+
const { durationSeconds, orgCapSeconds } = await getEffectiveSessionDuration(user.id, $replica);
3132
const sessionDurationOptions = getAllowedSessionOptions(orgCapSeconds, durationSeconds);
3233

3334
return typedjson({

apps/webapp/app/routes/auth.github.callback.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { LoaderFunction } from "@remix-run/node";
22
import { redirect } from "@remix-run/node";
33
import { prisma } from "~/db.server";
4-
import { getSession, redirectWithErrorMessage } from "~/models/message.server";
4+
import { redirectWithErrorMessage } from "~/models/message.server";
55
import { authenticator } from "~/services/auth.server";
66
import { setLastAuthMethodHeader } from "~/services/lastAuthMethod.server";
7-
import { commitSession } from "~/services/sessionStorage.server";
7+
import { commitSession, getUserSession } from "~/services/sessionStorage.server";
88
import { commitAuthenticatedSession } from "~/services/sessionDuration.server";
99
import { trackAndClearReferralSource } from "~/services/referralSource.server";
1010
import { redirectCookie } from "./auth.github";
@@ -19,7 +19,7 @@ export let loader: LoaderFunction = async ({ request }) => {
1919
failureRedirect: "/login", // If auth fails, the failureRedirect will be thrown as a Response
2020
});
2121

22-
const session = await getSession(request.headers.get("cookie"));
22+
const session = await getUserSession(request);
2323

2424
const userRecord = await prisma.user.findFirst({
2525
where: {

apps/webapp/app/routes/auth.google.callback.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { LoaderFunction } from "@remix-run/node";
22
import { redirect } from "@remix-run/node";
33
import { prisma } from "~/db.server";
4-
import { getSession, redirectWithErrorMessage } from "~/models/message.server";
4+
import { redirectWithErrorMessage } from "~/models/message.server";
55
import { authenticator } from "~/services/auth.server";
66
import { setLastAuthMethodHeader } from "~/services/lastAuthMethod.server";
7-
import { commitSession } from "~/services/sessionStorage.server";
7+
import { commitSession, getUserSession } from "~/services/sessionStorage.server";
88
import { commitAuthenticatedSession } from "~/services/sessionDuration.server";
99
import { trackAndClearReferralSource } from "~/services/referralSource.server";
1010
import { redirectCookie } from "./auth.google";
@@ -19,7 +19,7 @@ export let loader: LoaderFunction = async ({ request }) => {
1919
failureRedirect: "/login", // If auth fails, the failureRedirect will be thrown as a Response
2020
});
2121

22-
const session = await getSession(request.headers.get("cookie"));
22+
const session = await getUserSession(request);
2323

2424
const userRecord = await prisma.user.findFirst({
2525
where: {

apps/webapp/app/routes/resources.account.mfa.setup/MfaToggle.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function MfaToggle({ isEnabled, onToggle }: MfaToggleProps) {
1414
<Form method="post" className="w-full">
1515
<div className="flex w-full items-center justify-between gap-4">
1616
<InputGroup className="flex-1">
17-
<Label>Multi-factor authentication</Label>
17+
<Label htmlFor="mfa">Multi-factor authentication</Label>
1818
<Paragraph variant="small">
1919
Require a one-time code from your authenticator app (TOTP).
2020
</Paragraph>

apps/webapp/app/services/session.server.ts

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { redirect } from "@remix-run/node";
2+
import { $replica } from "~/db.server";
23
import { getUserById } from "~/models/user.server";
34
import { sanitizeRedirectPath } from "~/utils";
45
import { authenticator } from "./auth.server";
@@ -11,6 +12,44 @@ import {
1112
} from "./sessionDuration.server";
1213
import { 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+
1453
export 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

apps/webapp/app/services/sessionDuration.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export async function getEffectiveSessionDuration(
7474
client: PrismaClientOrTransaction = prisma
7575
): Promise<EffectiveSessionDuration> {
7676
const [user, orgCap] = await Promise.all([
77-
client.user.findUnique({
77+
client.user.findFirst({
7878
where: { id: userId },
7979
select: { sessionDuration: true },
8080
}),

apps/webapp/app/utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ const DEFAULT_REDIRECT = "/";
55

66
// Pathnames that are NOT user-navigable destinations: fetcher endpoints,
77
// OAuth/auth callbacks, JSON APIs, the magic-link redemption route, and the
8-
// auth flow routes themselves (which would create a redirect loop).
9-
const NON_NAVIGABLE_PREFIXES = ["/resources/", "/auth/", "/admin/", "/api/", "/engine/"];
8+
// auth flow routes themselves (which would create a redirect loop). Note
9+
// `/admin/api/` covers admin JSON endpoints while leaving `/admin`,
10+
// `/admin/back-office/*`, `/admin/orgs`, etc. navigable.
11+
const NON_NAVIGABLE_PREFIXES = ["/resources/", "/auth/", "/admin/api/", "/api/", "/engine/"];
1012
const NON_NAVIGABLE_EXACT = new Set(["/magic", "/logout", "/login", "/login/magic", "/login/mfa"]);
1113

1214
function isNavigablePath(pathname: string): boolean {

0 commit comments

Comments
 (0)