// SECURE: Adaptive MFA and behavioral analysis system
const crypto = require('crypto');
const geoip = require('geoip-lite');
const UAParser = require('ua-parser-js');
class AdaptiveMFASystem {
constructor() {
this.riskThresholds = {
low: 25,
medium: 50,
high: 75,
critical: 90
};
this.mfaRequirements = {
low: [], // No MFA required
medium: ['email_otp'], // Email OTP
high: ['email_otp', 'sms_otp'], // Email + SMS
critical: ['email_otp', 'sms_otp', 'totp'] // Full MFA
};
}
// Comprehensive risk assessment for login attempts
async assessLoginRisk(loginData) {
const {
email,
ip,
userAgent,
timestamp,
password,
fingerprint
} = loginData;
const riskFactors = {
geolocation: await this.assessGeolocationRisk(email, ip),
device: await this.assessDeviceRisk(email, userAgent, fingerprint),
temporal: await this.assessTemporalRisk(email, timestamp),
behavioral: await this.assessBehavioralRisk(email, loginData),
credentialPattern: await this.assessCredentialPatternRisk(email, password, ip),
networkReputation: await this.assessNetworkReputationRisk(ip)
};
// Calculate composite risk score
const totalRisk = Object.values(riskFactors).reduce((sum, factor) => {
return sum + (factor.score * factor.weight);
}, 0);
const riskLevel = this.determineRiskLevel(totalRisk);
const requiredMFA = this.mfaRequirements[riskLevel];
// Log risk assessment
await this.logRiskAssessment({
email,
ip,
riskFactors,
totalRisk,
riskLevel,
requiredMFA,
timestamp: new Date()
});
return {
riskScore: totalRisk,
riskLevel,
riskFactors,
mfaRequired: requiredMFA.length > 0,
requiredMethods: requiredMFA,
allowLogin: riskLevel !== 'critical'
};
}
// Assess geolocation-based risk
async assessGeolocationRisk(email, ip) {
const geo = geoip.lookup(ip);
let score = 0;
const factors = [];
if (!geo) {
score += 20;
factors.push('unknown_location');
} else {
// Get user's historical locations
const historicalLocations = await this.getUserHistoricalLocations(email);
if (historicalLocations.length > 0) {
const isKnownLocation = historicalLocations.some(loc =>
loc.country === geo.country &&
this.calculateDistance(loc, geo) < 100 // 100km radius
);
if (!isKnownLocation) {
score += 30;
factors.push('new_location');
// Check if location is in high-risk country
const isHighRiskCountry = await this.isHighRiskCountry(geo.country);
if (isHighRiskCountry) {
score += 20;
factors.push('high_risk_country');
}
// Check for impossible travel
const lastLocation = historicalLocations[0];
if (lastLocation && this.isImpossibleTravel(lastLocation, geo)) {
score += 40;
factors.push('impossible_travel');
}
}
}
// Update location history
await this.updateLocationHistory(email, geo);
}
return {
score: Math.min(score, 100),
weight: 0.25,
factors,
details: { geo, historicalCount: (await this.getUserHistoricalLocations(email)).length }
};
}
// Assess device and browser fingerprint risk
async assessDeviceRisk(email, userAgent, fingerprint) {
let score = 0;
const factors = [];
const parser = new UAParser(userAgent);
const deviceInfo = {
browser: parser.getBrowser(),
os: parser.getOS(),
device: parser.getDevice(),
fingerprint
};
// Get user's known devices
const knownDevices = await this.getUserKnownDevices(email);
// Check if device fingerprint is known
const isKnownDevice = knownDevices.some(device =>
device.fingerprint === fingerprint
);
if (!isKnownDevice) {
score += 25;
factors.push('new_device');
// Check for suspicious device characteristics
if (!deviceInfo.browser.name || !deviceInfo.os.name) {
score += 15;
factors.push('incomplete_device_info');
}
// Check for automation indicators
if (this.detectAutomationIndicators(userAgent)) {
score += 30;
factors.push('automation_detected');
}
} else {
// Check if device behavior has changed
const deviceBehaviorScore = await this.assessDeviceBehaviorChange(
email,
fingerprint,
deviceInfo
);
score += deviceBehaviorScore;
if (deviceBehaviorScore > 0) {
factors.push('device_behavior_change');
}
}
// Update device registry
await this.updateDeviceRegistry(email, deviceInfo);
return {
score: Math.min(score, 100),
weight: 0.20,
factors,
details: { deviceInfo, isKnownDevice, knownDeviceCount: knownDevices.length }
};
}
// Assess temporal patterns and timing risk
async assessTemporalRisk(email, timestamp) {
let score = 0;
const factors = [];
const loginTime = new Date(timestamp);
// Get user's historical login patterns
const loginPatterns = await this.getUserLoginPatterns(email);
if (loginPatterns) {
// Check if login is outside normal hours
const hour = loginTime.getHours();
const isNormalTime = hour >= loginPatterns.normalHours.start &&
hour <= loginPatterns.normalHours.end;
if (!isNormalTime) {
score += 15;
factors.push('unusual_time');
}
// Check if login is on unusual day
const dayOfWeek = loginTime.getDay();
if (!loginPatterns.normalDays.includes(dayOfWeek)) {
score += 10;
factors.push('unusual_day');
}
// Check frequency anomalies
const recentLogins = await this.getRecentLogins(email, 24 * 60 * 60 * 1000); // 24 hours
if (recentLogins.length > loginPatterns.averageDailyLogins * 3) {
score += 25;
factors.push('high_frequency');
}
}
// Update login patterns
await this.updateLoginPatterns(email, loginTime);
return {
score: Math.min(score, 100),
weight: 0.15,
factors,
details: { hour: loginTime.getHours(), dayOfWeek: loginTime.getDay() }
};
}
// Assess behavioral patterns
async assessBehavioralRisk(email, loginData) {
let score = 0;
const factors = [];
// Analyze typing patterns (if available)
if (loginData.typingPatterns) {
const typingScore = await this.assessTypingPatterns(email, loginData.typingPatterns);
score += typingScore;
if (typingScore > 20) {
factors.push('unusual_typing_pattern');
}
}
// Analyze session behavior
if (loginData.sessionBehavior) {
const behaviorScore = await this.assessSessionBehavior(email, loginData.sessionBehavior);
score += behaviorScore;
if (behaviorScore > 15) {
factors.push('unusual_session_behavior');
}
}
// Check for velocity attacks
const velocityScore = await this.assessLoginVelocity(email, loginData.ip);
score += velocityScore;
if (velocityScore > 20) {
factors.push('high_login_velocity');
}
return {
score: Math.min(score, 100),
weight: 0.20,
factors,
details: { hasTypingData: !!loginData.typingPatterns }
};
}
// Check for credential pattern risks
async assessCredentialPatternRisk(email, password, ip) {
let score = 0;
const factors = [];
// Check if password appears in breach databases
const isBreachedPassword = await this.checkPasswordBreach(password);
if (isBreachedPassword) {
score += 40;
factors.push('breached_password');
}
// Check for credential stuffing patterns
const credentialStuffingScore = await this.checkCredentialStuffingPattern(email, ip);
score += credentialStuffingScore;
if (credentialStuffingScore > 25) {
factors.push('credential_stuffing_pattern');
}
// Check password reuse patterns
const reuseScore = await this.checkPasswordReusePattern(email, password);
score += reuseScore;
if (reuseScore > 0) {
factors.push('password_reuse_detected');
}
return {
score: Math.min(score, 100),
weight: 0.15,
factors,
details: { isBreachedPassword, credentialStuffingScore }
};
}
// Initiate adaptive MFA based on risk assessment
async initiateMFA(email, requiredMethods, riskAssessment) {
const mfaSession = {
sessionId: crypto.randomUUID(),
email,
requiredMethods: [...requiredMethods],
completedMethods: [],
riskAssessment,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 10 * 60 * 1000), // 10 minutes
attempts: 0,
maxAttempts: 3
};
// Store MFA session
await redisClient.setex(
`mfa_session:${mfaSession.sessionId}`,
600,
JSON.stringify(mfaSession)
);
// Send MFA challenges
const challenges = [];
for (const method of requiredMethods) {
const challenge = await this.sendMFAChallenge(email, method, mfaSession.sessionId);
challenges.push(challenge);
}
return {
sessionId: mfaSession.sessionId,
requiredMethods,
challenges,
expiresAt: mfaSession.expiresAt
};
}
// Send MFA challenge based on method
async sendMFAChallenge(email, method, sessionId) {
const challenge = {
method,
challengeId: crypto.randomUUID(),
sessionId,
createdAt: new Date()
};
switch (method) {
case 'email_otp':
const emailOTP = this.generateOTP(6);
await this.sendEmailOTP(email, emailOTP, sessionId);
challenge.masked = this.maskEmail(email);
break;
case 'sms_otp':
const user = await this.getUserByEmail(email);
if (user?.phoneNumber) {
const smsOTP = this.generateOTP(6);
await this.sendSMSOTP(user.phoneNumber, smsOTP, sessionId);
challenge.masked = this.maskPhoneNumber(user.phoneNumber);
}
break;
case 'totp':
challenge.qrCode = await this.generateTOTPQR(email, sessionId);
break;
}
return challenge;
}
}