Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/auth/mfa/step-up.server.test.ts
T

151 lines
5.3 KiB
TypeScript

// @vitest-environment node
import { beforeEach, describe, expect, test, vi } from "vitest";
vi.mock("~/utils/session.server", async () => {
const { createCookieSessionStorage } = await import("react-router");
return {
storage: createCookieSessionStorage({
cookie: { name: "test_session", secrets: ["test-session-secret"], path: "/", httpOnly: true, sameSite: "lax" },
}),
};
});
import { storage } from "~/utils/session.server";
import {
checkMfaStepUpState,
clearMfaStepUpState,
getMfaStepUpState,
MFA_STEP_UP_MAX_AGE_MS,
MFA_STEP_UP_SESSION_KEY,
setMfaStepUpState,
} from "./step-up.server";
async function createSession() {
return storage.getSession();
}
describe("step-up.server", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("records and verifies fresh MFA step-up state for the same user", async () => {
const session = await createSession();
const verifiedAt = new Date("2026-05-08T14:00:00.000Z");
const state = setMfaStepUpState(session, {
userId: "admin-1",
method: "totp",
redirectTo: "/admin?tab=security",
now: verifiedAt,
});
const result = checkMfaStepUpState(session, "admin-1", { now: new Date(verifiedAt.getTime() + 30_000) });
expect(state).toEqual({
userId: "admin-1",
method: "totp",
verifiedAt: verifiedAt.toISOString(),
redirectTo: "/admin?tab=security",
});
expect(getMfaStepUpState(session)).toEqual(state);
expect(result).toEqual({ verified: true, state, ageMs: 30_000 });
});
test("records step-up state without optional redirect metadata", async () => {
const session = await createSession();
const verifiedAt = new Date("2026-05-08T14:00:00.000Z");
const state = setMfaStepUpState(session, {
userId: "admin-1",
method: "recovery-code",
now: verifiedAt,
});
expect(state).toEqual({
userId: "admin-1",
method: "recovery-code",
verifiedAt: verifiedAt.toISOString(),
});
expect(getMfaStepUpState(session)).toEqual(state);
});
test("fails closed for stale and future timestamps", async () => {
const staleSession = await createSession();
const futureSession = await createSession();
const now = new Date("2026-05-08T14:10:00.000Z");
setMfaStepUpState(staleSession, {
userId: "admin-1",
method: "recovery-code",
now: new Date(now.getTime() - MFA_STEP_UP_MAX_AGE_MS - 1),
});
setMfaStepUpState(futureSession, {
userId: "admin-1",
method: "totp",
now: new Date(now.getTime() + 1),
});
expect(checkMfaStepUpState(staleSession, "admin-1", { now })).toEqual({ verified: false, reason: "stale" });
expect(checkMfaStepUpState(futureSession, "admin-1", { now })).toEqual({ verified: false, reason: "stale" });
});
test("does not authorize a different user", async () => {
const session = await createSession();
setMfaStepUpState(session, { userId: "admin-1", method: "totp", now: new Date("2026-05-08T14:00:00.000Z") });
expect(checkMfaStepUpState(session, "admin-2", { now: new Date("2026-05-08T14:01:00.000Z") })).toEqual({
verified: false,
reason: "wrong-user",
});
});
test("treats missing and malformed state as unverified without throwing", async () => {
const missingSession = await createSession();
const malformedPayloads = [
{ method: "totp", verifiedAt: "2026-05-08T14:00:00.000Z" },
{ userId: "", method: "totp", verifiedAt: "2026-05-08T14:00:00.000Z" },
"not-json-object",
{ userId: "admin-1", method: "sms", verifiedAt: "2026-05-08T14:00:00.000Z" },
{ userId: "admin-1", verifiedAt: "2026-05-08T14:00:00.000Z" },
{ userId: "admin-1", method: "totp", verifiedAt: "not-a-date" },
{ userId: "admin-1", method: "totp", verifiedAt: "2026-05-08T14:00:00.000Z", redirectTo: 123 },
];
expect(checkMfaStepUpState(missingSession, "admin-1")).toEqual({ verified: false, reason: "missing" });
for (const payload of malformedPayloads) {
const session = await createSession();
session.set(MFA_STEP_UP_SESSION_KEY, payload);
expect(() => getMfaStepUpState(session)).not.toThrow();
expect(getMfaStepUpState(session)).toBeNull();
expect(checkMfaStepUpState(session, "admin-1")).toEqual({ verified: false, reason: "malformed" });
}
});
test("clears existing MFA step-up state", async () => {
const session = await createSession();
setMfaStepUpState(session, { userId: "admin-1", method: "totp", now: new Date("2026-05-08T14:00:00.000Z") });
clearMfaStepUpState(session);
expect(getMfaStepUpState(session)).toBeNull();
expect(checkMfaStepUpState(session, "admin-1")).toEqual({ verified: false, reason: "missing" });
});
test("serialized step-up state contains no cookies, tokens, hashes, passwords, or factor secrets", async () => {
const session = await createSession();
const state = setMfaStepUpState(session, {
userId: "admin-1",
method: "recovery-code",
redirectTo: "/admin",
now: new Date("2026-05-08T14:00:00.000Z"),
});
const serialized = JSON.stringify(state);
expect(serialized).toContain("admin-1");
expect(serialized).toContain("recovery-code");
expect(serialized).not.toMatch(/cookie|csrf|token|secret|hash|password|encrypted|totpSecret|recoveryCodes/i);
});
});