61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
import type { AuthRiskDecision, AuthRiskSignals, AuthRiskLevel } from "./types.ts";
|
|
|
|
export interface RiskPolicy {
|
|
captchaThreshold: number;
|
|
mfaThreshold: number;
|
|
blockThreshold: number;
|
|
}
|
|
|
|
function finiteScore(value: unknown, fallback = 0): number {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
}
|
|
|
|
function threshold(value: number, fallback: number): number {
|
|
return Math.min(100, Math.max(0, finiteScore(value, fallback)));
|
|
}
|
|
|
|
function level(score: number): AuthRiskLevel {
|
|
if (score >= 90) return "critical";
|
|
if (score >= 65) return "high";
|
|
if (score >= 35) return "medium";
|
|
return "low";
|
|
}
|
|
|
|
export function evaluateAuthRisk(
|
|
signals: AuthRiskSignals = {},
|
|
policy: RiskPolicy = { captchaThreshold: 35, mfaThreshold: 60, blockThreshold: 90 },
|
|
): AuthRiskDecision {
|
|
let score = Math.max(0, finiteScore(signals.customScore));
|
|
const reasons: string[] = [];
|
|
const add = (condition: boolean | undefined, points: number, reason: string) => {
|
|
if (!condition) return;
|
|
score += points;
|
|
reasons.push(reason);
|
|
};
|
|
const failedAttempts = Math.max(0, Math.floor(finiteScore(signals.failedAttempts)));
|
|
if (failedAttempts > 0) {
|
|
score += Math.min(45, failedAttempts * 10);
|
|
reasons.push("failed-attempts");
|
|
}
|
|
add(signals.unfamiliarDevice, 18, "unfamiliar-device");
|
|
add(signals.unusualIp, 20, "unusual-ip");
|
|
add(signals.impossibleTravel, 35, "impossible-travel");
|
|
add(signals.breachedPassword, 45, "breached-password");
|
|
add(signals.automationSuspected, 40, "automation-suspected");
|
|
add(signals.accountLocked, 100, "account-locked");
|
|
score = Math.min(100, Math.max(0, score));
|
|
|
|
const captchaThreshold = threshold(policy.captchaThreshold, 35);
|
|
const mfaThreshold = threshold(policy.mfaThreshold, 60);
|
|
const blockThreshold = threshold(policy.blockThreshold, 90);
|
|
|
|
return {
|
|
score,
|
|
level: level(score),
|
|
requireCaptcha: score >= captchaThreshold,
|
|
requireMfa: score >= mfaThreshold,
|
|
block: score >= blockThreshold,
|
|
reasons,
|
|
};
|
|
}
|