mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
Wire the previously-unused mfaEnforced flag into a real policy. An admin can require a second factor instance-wide (an AppSettings toggle in the General section) or per user (a toggle in user management); the effective requirement is the OR of the two. When required and the account has no active factor — no confirmed TOTP, no registered security key, and no primary passkey — the user is walled behind a forced-enrollment interstitial after sign-in, with only the security settings and sign-out reachable until a factor is enrolled. The gate is surfaced like the onboarding gate: a hint cookie the proxy reads without a DB round-trip, recomputed from the database on every sign-in and on /api/auth/me so a tampered cookie self-corrects. Nobody is locked out — the enrollment surface stays reachable, and toggling the policy off (or the disable-mfa CLI) is the operator's recovery path.
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
vi.mock("@/lib/db", () => ({
|
|
prisma: {
|
|
appSettings: { findUnique: vi.fn() },
|
|
webauthnMfaCredential: { count: vi.fn() },
|
|
passkey: { count: vi.fn() },
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/lib/auth/secure-cookie", () => ({
|
|
shouldEmitSecureCookie: () => true,
|
|
}));
|
|
|
|
const cookieStore = { set: vi.fn(), delete: vi.fn(), get: vi.fn() };
|
|
vi.mock("next/headers", () => ({
|
|
cookies: vi.fn(async () => cookieStore),
|
|
}));
|
|
|
|
import { resolveMfaEnrollmentRequired } from "../mfa-enrollment";
|
|
import { prisma } from "@/lib/db";
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
|
|
mfaRequired: false,
|
|
} as never);
|
|
vi.mocked(prisma.webauthnMfaCredential.count).mockResolvedValue(0 as never);
|
|
vi.mocked(prisma.passkey.count).mockResolvedValue(0 as never);
|
|
});
|
|
|
|
describe("resolveMfaEnrollmentRequired", () => {
|
|
it("returns false (and skips all DB reads) for an account with confirmed TOTP", async () => {
|
|
const required = await resolveMfaEnrollmentRequired("u1", {
|
|
totpConfirmedAt: new Date(),
|
|
mfaEnforced: true,
|
|
});
|
|
expect(required).toBe(false);
|
|
expect(prisma.appSettings.findUnique).not.toHaveBeenCalled();
|
|
expect(prisma.webauthnMfaCredential.count).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns false when no policy applies, even for a factor-less account", async () => {
|
|
const required = await resolveMfaEnrollmentRequired("u1", {
|
|
totpConfirmedAt: null,
|
|
mfaEnforced: false,
|
|
});
|
|
expect(required).toBe(false);
|
|
// Policy off short-circuits before counting factors.
|
|
expect(prisma.webauthnMfaCredential.count).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("forces enrollment under the instance policy for a factor-less account", async () => {
|
|
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
|
|
mfaRequired: true,
|
|
} as never);
|
|
const required = await resolveMfaEnrollmentRequired("u1", {
|
|
totpConfirmedAt: null,
|
|
mfaEnforced: false,
|
|
});
|
|
expect(required).toBe(true);
|
|
});
|
|
|
|
it("forces enrollment under a per-user override even with the instance policy off", async () => {
|
|
const required = await resolveMfaEnrollmentRequired("u1", {
|
|
totpConfirmedAt: null,
|
|
mfaEnforced: true,
|
|
});
|
|
expect(required).toBe(true);
|
|
});
|
|
|
|
it("lets a registered security key satisfy the policy", async () => {
|
|
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
|
|
mfaRequired: true,
|
|
} as never);
|
|
vi.mocked(prisma.webauthnMfaCredential.count).mockResolvedValue(1 as never);
|
|
const required = await resolveMfaEnrollmentRequired("u1", {
|
|
totpConfirmedAt: null,
|
|
mfaEnforced: false,
|
|
});
|
|
expect(required).toBe(false);
|
|
});
|
|
|
|
it("lets a primary passkey satisfy the policy", async () => {
|
|
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
|
|
mfaRequired: true,
|
|
} as never);
|
|
vi.mocked(prisma.passkey.count).mockResolvedValue(1 as never);
|
|
const required = await resolveMfaEnrollmentRequired("u1", {
|
|
totpConfirmedAt: null,
|
|
mfaEnforced: false,
|
|
});
|
|
expect(required).toBe(false);
|
|
});
|
|
});
|