198 lines
6.8 KiB
TypeScript
198 lines
6.8 KiB
TypeScript
// @vitest-environment node
|
|
import { describe, expect, test } from "vitest";
|
|
import type { UserTotpStatus } from "~/utils/db/users.db.server";
|
|
import type { MfaStepUpCheckResult } from "./step-up.server";
|
|
import {
|
|
ADMIN_MFA_CHALLENGE_PATH,
|
|
enforceAdminMfa,
|
|
type AdminMfaEnforcementResult,
|
|
} from "./enforcement.server";
|
|
|
|
const enabledTotp: UserTotpStatus = {
|
|
enabled: true,
|
|
pending: false,
|
|
enabledAt: new Date("2026-05-08T14:00:00.000Z"),
|
|
pendingCreatedAt: null,
|
|
};
|
|
|
|
const pendingOnlyTotp: UserTotpStatus = {
|
|
enabled: false,
|
|
pending: true,
|
|
enabledAt: null,
|
|
pendingCreatedAt: new Date("2026-05-08T14:00:00.000Z"),
|
|
};
|
|
|
|
const freshStepUp: MfaStepUpCheckResult = {
|
|
verified: true,
|
|
state: { userId: "admin-1", method: "totp", verifiedAt: "2026-05-08T14:01:00.000Z" },
|
|
ageMs: 1_000,
|
|
};
|
|
|
|
function enforce(overrides: Partial<Parameters<typeof enforceAdminMfa>[0]> = {}): AdminMfaEnforcementResult {
|
|
return enforceAdminMfa({
|
|
requestUrl: "https://admin.example.com/users?tenant=tenant-1#section",
|
|
userId: "admin-1",
|
|
isAdmin: true,
|
|
totpStatus: enabledTotp,
|
|
stepUp: freshStepUp,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
describe("enforceAdminMfa", () => {
|
|
test("allows admin users with enabled TOTP and fresh step-up", () => {
|
|
expect(enforce()).toEqual({
|
|
allowed: true,
|
|
reason: "allowed",
|
|
redirectTo: "/users?tenant=tenant-1#section",
|
|
});
|
|
});
|
|
|
|
test("allows admins without active TOTP so the profile enrollment path is opt-in and non-blocking", () => {
|
|
expect(enforce({ totpStatus: null })).toEqual({
|
|
allowed: true,
|
|
reason: "allowed",
|
|
redirectTo: "/users?tenant=tenant-1#section",
|
|
});
|
|
expect(enforce({ totpStatus: { enabled: false, pending: false, enabledAt: null, pendingCreatedAt: null } })).toEqual({
|
|
allowed: true,
|
|
reason: "allowed",
|
|
redirectTo: "/users?tenant=tenant-1#section",
|
|
});
|
|
expect(enforce({ totpStatus: pendingOnlyTotp })).toEqual({
|
|
allowed: true,
|
|
reason: "allowed",
|
|
redirectTo: "/users?tenant=tenant-1#section",
|
|
});
|
|
});
|
|
|
|
test("requires challenge when step-up is missing", () => {
|
|
expect(enforce({ stepUp: { verified: false, reason: "missing" } })).toEqual({
|
|
allowed: false,
|
|
reason: "step-up-missing",
|
|
redirectTo: `${ADMIN_MFA_CHALLENGE_PATH}?redirectTo=%2Fusers%3Ftenant%3Dtenant-1%23section`,
|
|
});
|
|
});
|
|
|
|
test("accepts URL objects when deriving safe redirects and challenge targets", () => {
|
|
expect(enforce({ requestUrl: new URL("https://admin.example.com/users?tenant=tenant-1#section") })).toEqual({
|
|
allowed: true,
|
|
reason: "allowed",
|
|
redirectTo: "/users?tenant=tenant-1#section",
|
|
});
|
|
expect(
|
|
enforce({
|
|
requestUrl: new URL("https://admin.example.com/users?tenant=tenant-1#section"),
|
|
stepUp: { verified: false, reason: "missing" },
|
|
}),
|
|
).toEqual({
|
|
allowed: false,
|
|
reason: "step-up-missing",
|
|
redirectTo: `${ADMIN_MFA_CHALLENGE_PATH}?redirectTo=%2Fusers%3Ftenant%3Dtenant-1%23section`,
|
|
});
|
|
});
|
|
|
|
test("allows the admin MFA challenge path when requestUrl is already a URL object", () => {
|
|
expect(
|
|
enforce({
|
|
requestUrl: new URL("https://admin.example.com/mfa-challenge?redirectTo=%2Fusers"),
|
|
stepUp: { verified: false, reason: "missing" },
|
|
}),
|
|
).toEqual({
|
|
allowed: true,
|
|
reason: "allowed",
|
|
redirectTo: "/mfa-challenge?redirectTo=%2Fusers",
|
|
});
|
|
});
|
|
|
|
test("requires challenge when step-up is stale", () => {
|
|
expect(enforce({ stepUp: { verified: false, reason: "stale" } })).toEqual({
|
|
allowed: false,
|
|
reason: "step-up-stale",
|
|
redirectTo: `${ADMIN_MFA_CHALLENGE_PATH}?redirectTo=%2Fusers%3Ftenant%3Dtenant-1%23section`,
|
|
});
|
|
});
|
|
|
|
test("uses canonical admin redirects on the admin subdomain", () => {
|
|
expect(enforce({ stepUp: { verified: false, reason: "missing" }, redirectMode: "host-aware" })).toEqual({
|
|
allowed: false,
|
|
reason: "step-up-missing",
|
|
redirectTo: "/mfa-challenge?redirectTo=%2Fusers%3Ftenant%3Dtenant-1%23section",
|
|
});
|
|
|
|
expect(enforce({ redirectMode: "host-aware" })).toEqual({
|
|
allowed: true,
|
|
reason: "allowed",
|
|
redirectTo: "/users?tenant=tenant-1#section",
|
|
});
|
|
|
|
expect(enforce({ totpStatus: null, redirectMode: "host-aware" })).toEqual({
|
|
allowed: true,
|
|
reason: "allowed",
|
|
redirectTo: "/users?tenant=tenant-1#section",
|
|
});
|
|
});
|
|
|
|
test("allows the admin MFA enrollment route before TOTP is enabled", () => {
|
|
expect(enforce({ requestUrl: "https://admin.example.com/settings/profile?mfa=setup", totpStatus: pendingOnlyTotp, redirectMode: "host-aware" })).toEqual({
|
|
allowed: true,
|
|
reason: "allowed",
|
|
redirectTo: "/settings/profile?mfa=setup",
|
|
});
|
|
});
|
|
|
|
test("allows the admin MFA challenge route without existing step-up", () => {
|
|
expect(enforce({ requestUrl: "https://admin.example.com/mfa-challenge?redirectTo=%2Fusers", stepUp: { verified: false, reason: "missing" } })).toEqual({
|
|
allowed: true,
|
|
reason: "allowed",
|
|
redirectTo: "/mfa-challenge?redirectTo=%2Fusers",
|
|
});
|
|
});
|
|
|
|
test("denies non-admin and anonymous requests before challenge bypass is possible", () => {
|
|
expect(enforce({ isAdmin: false, stepUp: freshStepUp })).toEqual({
|
|
allowed: false,
|
|
reason: "missing-admin",
|
|
redirectTo: "/",
|
|
});
|
|
expect(enforce({ userId: null, isAdmin: true, stepUp: freshStepUp })).toEqual({
|
|
allowed: false,
|
|
reason: "missing-user",
|
|
redirectTo: "/login",
|
|
});
|
|
});
|
|
|
|
test("sanitizes redirect targets derived from external and malformed request URLs", () => {
|
|
expect(enforce({ requestUrl: "https://evil.example/phish?next=https://admin.example.com/settings" })).toEqual({
|
|
allowed: true,
|
|
reason: "allowed",
|
|
redirectTo: "/phish?next=https://admin.example.com/settings",
|
|
});
|
|
expect(enforce({ requestUrl: "not a url", stepUp: { verified: false, reason: "malformed" } })).toEqual({
|
|
allowed: false,
|
|
reason: "step-up-malformed",
|
|
redirectTo: `${ADMIN_MFA_CHALLENGE_PATH}?redirectTo=%2F`,
|
|
});
|
|
});
|
|
|
|
test("maps malformed and wrong-user step-up states to fail-closed reasons", () => {
|
|
expect(enforce({ stepUp: { verified: false, reason: "malformed" } })).toMatchObject({
|
|
allowed: false,
|
|
reason: "step-up-malformed",
|
|
});
|
|
expect(enforce({ stepUp: { verified: false, reason: "wrong-user" } })).toMatchObject({
|
|
allowed: false,
|
|
reason: "step-up-wrong-user",
|
|
});
|
|
});
|
|
|
|
test("result payloads contain no TOTP secrets, recovery codes, hashes, cookies, tokens, or passwords", () => {
|
|
const result = enforce({
|
|
requestUrl: "https://admin.example.com/dashboard?tab=secrets",
|
|
stepUp: { verified: false, reason: "missing" },
|
|
});
|
|
|
|
expect(JSON.stringify(result)).not.toMatch(/totpSecret|recoveryCode|hash|cookie|token|password|encrypted/i);
|
|
});
|
|
});
|