Files

86 lines
4.6 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
ADMIN_MFA_PROOF_TTL_MS,
issueAdminMfaProof,
resolveAdminMfaStatus,
verifyAdminMfaProof,
} from "../src/lib/admin/auth-policy.ts";
const now = 1_786_000_000_000;
const context = {
userId: "11111111-1111-4111-8111-111111111111",
sessionId: "22222222-2222-4222-8222-222222222222",
origin: "https://admin.staging.jyotisha.chat",
};
const sessionToken = "better-auth-session-token-held-server-side";
const proofSecret = "admin-proof-secret-held-only-by-the-server";
test("MFA proof is server-signed, short-lived, and bound to the current user, session, origin, and session token", () => {
const proof = issueAdminMfaProof(context, proofSecret, sessionToken, now);
assert.equal(verifyAdminMfaProof(proof, context, proofSecret, sessionToken, now + 1_000), true);
assert.equal(verifyAdminMfaProof(proof, { ...context, userId: "other-user" }, proofSecret, sessionToken, now + 1_000), false);
assert.equal(verifyAdminMfaProof(proof, { ...context, sessionId: "other-session" }, proofSecret, sessionToken, now + 1_000), false);
assert.equal(verifyAdminMfaProof(proof, { ...context, origin: "https://evil.example" }, proofSecret, sessionToken, now + 1_000), false);
assert.equal(verifyAdminMfaProof(proof, context, "wrong-server-secret-that-is-long-enough", sessionToken, now + 1_000), false);
assert.equal(verifyAdminMfaProof(proof, context, proofSecret, "rotated-session-token", now + 1_000), false);
assert.equal(verifyAdminMfaProof(proof, context, proofSecret, sessionToken, now + ADMIN_MFA_PROOF_TTL_MS), false);
});
test("highRiskWritesEnabled means the current session has the MFA prerequisite, not email proof", () => {
assert.deepEqual(resolveAdminMfaStatus(false, false, false), {
required: false,
enrolled: false,
verified: false,
highRiskWritesEnabled: true,
});
assert.equal(resolveAdminMfaStatus(true, false, false).highRiskWritesEnabled, false);
assert.equal(resolveAdminMfaStatus(true, true, false).highRiskWritesEnabled, false);
assert.deepEqual(resolveAdminMfaStatus(true, true, true), {
required: true,
enrolled: true,
verified: true,
highRiskWritesEnabled: true,
});
});
test("MFA API keeps native Better Auth enrollment, recovery, rotation, and proof boundaries", () => {
const route = readFileSync(new URL("../src/app/api/admin/mfa/route.ts", import.meta.url), "utf8");
const authFactory = readFileSync(new URL("../src/modules/identity/auth-factory.ts", import.meta.url), "utf8");
const migration = readFileSync(new URL("../db/migrations/20260806070000_admin_mfa.sql", import.meta.url), "utf8");
assert.match(authFactory, /twoFactor\(/);
assert.doesNotMatch(authFactory, /skipVerificationOnEnable\s*:\s*true/);
assert.match(route, /enableTwoFactor/);
assert.match(route, /verifyTOTP/);
assert.match(route, /verifyBackupCode/);
assert.match(route, /generateBackupCodes/);
assert.match(route, /disableTwoFactor/);
assert.match(route, /headersAfterNativeResponse/);
assert.match(route, /copyNativeCookies\(nativeResponse, response\)/);
assert.match(route, /requireVerifiedMfa\(request, session\)/);
assert.match(route, /ADMIN_MFA_PROOF_COOKIE/);
assert.match(route, /HIGH_RISK_ADMIN_PROOF_COOKIE/);
assert.match(route, /adminProofSigningSecret\(\)/);
assert.doesNotMatch(route, /console\.|logger|log.*(?:seed|secret|backup|totp)|(?:seed|secret|backup|totp).*log/i);
assert.match(migration, /revoke all on table identity\.two_factors[\s\S]*admin_runtime/);
assert.match(migration, /grant select, insert, update, delete on table identity\.two_factors[\s\S]*identity_runtime/);
});
test("high-risk UI uses permission-scoped email OTP without requiring MFA", () => {
const modal = readFileSync(new URL("../src/components/admin/reason-action-modal.tsx", import.meta.url), "utf8");
const reauth = readFileSync(new URL("../src/app/api/admin/reauth/route.ts", import.meta.url), "utf8");
const http = readFileSync(new URL("../src/lib/admin/http.ts", import.meta.url), "utf8");
assert.match(modal, /action: "request"/);
assert.match(modal, /action: "verify"/);
assert.match(modal, /邮箱验证码/);
assert.doesNotMatch(modal, /\/api\/admin\/mfa|\/admin\/security|MfaStatus|mfaFactor|mfaReady/);
assert.match(reauth, /requireAdminMutation\(request, parsed\.data\.permission\)[\s\S]*getIdentityEmailOtpApi/);
assert.doesNotMatch(reauth, /requireAdminMfaIfRequired/);
assert.match(http, /requireAdminMutation\(request, permission\)[\s\S]*verifyHighRiskAdminProof/);
assert.doesNotMatch(http, /requireAdminMfaIfRequired/);
});