88 lines
2.9 KiB
TypeScript
88 lines
2.9 KiB
TypeScript
import type { UserTotpStatus } from "~/utils/db/users.db.server";
|
|
import type { MfaStepUpCheckReason, MfaStepUpCheckResult } from "./step-up.server";
|
|
export { ADMIN_MFA_CHALLENGE_PATH, ADMIN_MFA_DEFAULT_REDIRECT_PATH, ADMIN_MFA_ENROLLMENT_PATH } from "./paths";
|
|
import { ADMIN_MFA_CHALLENGE_PATH, ADMIN_MFA_DEFAULT_REDIRECT_PATH } from "./paths";
|
|
|
|
export type AdminMfaEnforcementFailureReason =
|
|
| "missing-user"
|
|
| "missing-admin"
|
|
| "missing-totp-status"
|
|
| "totp-not-enabled"
|
|
| "step-up-missing"
|
|
| "step-up-malformed"
|
|
| "step-up-wrong-user"
|
|
| "step-up-stale";
|
|
|
|
export type AdminMfaEnforcementReason = "allowed" | AdminMfaEnforcementFailureReason;
|
|
|
|
export type AdminMfaEnforcementResult =
|
|
| { allowed: true; reason: "allowed"; redirectTo: string }
|
|
| { allowed: false; reason: AdminMfaEnforcementFailureReason; redirectTo: string };
|
|
|
|
export type AdminMfaEnforcementInput = {
|
|
requestUrl: string | URL;
|
|
userId?: string | null;
|
|
isAdmin?: boolean | null;
|
|
totpStatus?: UserTotpStatus | null;
|
|
stepUp: MfaStepUpCheckResult;
|
|
redirectMode?: "host-aware";
|
|
appFallbackPath?: string;
|
|
};
|
|
|
|
function isAdminMfaChallengePath(requestUrl: string | URL): boolean {
|
|
try {
|
|
const url = requestUrl instanceof URL ? requestUrl : new URL(requestUrl);
|
|
return url.pathname === ADMIN_MFA_CHALLENGE_PATH;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function safePathFromRequestUrl(requestUrl: string | URL): string {
|
|
try {
|
|
const url = requestUrl instanceof URL ? requestUrl : new URL(requestUrl);
|
|
return `${url.pathname}${url.search}${url.hash}`;
|
|
} catch {
|
|
return ADMIN_MFA_DEFAULT_REDIRECT_PATH;
|
|
}
|
|
}
|
|
|
|
function challengeRedirectTo(requestUrl: string | URL): string {
|
|
const redirectTo = safePathFromRequestUrl(requestUrl);
|
|
const params = new URLSearchParams({ redirectTo });
|
|
return `${ADMIN_MFA_CHALLENGE_PATH}?${params.toString()}`;
|
|
}
|
|
|
|
function stepUpReason(reason: MfaStepUpCheckReason): AdminMfaEnforcementFailureReason {
|
|
switch (reason) {
|
|
case "missing":
|
|
return "step-up-missing";
|
|
case "malformed":
|
|
return "step-up-malformed";
|
|
case "wrong-user":
|
|
return "step-up-wrong-user";
|
|
case "stale":
|
|
return "step-up-stale";
|
|
}
|
|
}
|
|
|
|
export function enforceAdminMfa(input: AdminMfaEnforcementInput): AdminMfaEnforcementResult {
|
|
if (!input.userId) {
|
|
return { allowed: false, reason: "missing-user", redirectTo: "/login" };
|
|
}
|
|
|
|
if (input.isAdmin !== true) {
|
|
return { allowed: false, reason: "missing-admin", redirectTo: input.appFallbackPath ?? "/" };
|
|
}
|
|
|
|
if (!input.totpStatus || !input.totpStatus.enabled) {
|
|
return { allowed: true, reason: "allowed", redirectTo: safePathFromRequestUrl(input.requestUrl) };
|
|
}
|
|
|
|
if (!input.stepUp.verified && !isAdminMfaChallengePath(input.requestUrl)) {
|
|
return { allowed: false, reason: stepUpReason(input.stepUp.reason), redirectTo: challengeRedirectTo(input.requestUrl) };
|
|
}
|
|
|
|
return { allowed: true, reason: "allowed", redirectTo: safePathFromRequestUrl(input.requestUrl) };
|
|
}
|