auth/src/services/riskScoring.js

268 lines
6.8 KiB
JavaScript

// src/services/riskScoring.js
// === SECURITY HARDENING: IP/DEVICE RISK ===
// Basic risk scoring for IP addresses and device fingerprinting
const crypto = require('crypto');
const db = require('../db');
// === SECURITY HARDENING: IP/DEVICE RISK ===
// Configuration for suspicious IP ranges
// Can be set via environment variable as comma-separated CIDR blocks
// Example: BLOCKED_IP_RANGES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
const BLOCKED_IP_RANGES = (process.env.BLOCKED_IP_RANGES || '')
.split(',')
.map(r => r.trim())
.filter(Boolean);
// === SECURITY HARDENING: IP/DEVICE RISK ===
// Whether to require OTP re-verification on suspicious refresh
const REQUIRE_OTP_ON_SUSPICIOUS_REFRESH = process.env.REQUIRE_OTP_ON_SUSPICIOUS_REFRESH === 'true';
/**
* Check if an IP address is in a blocked CIDR range
* Simple implementation for common private ranges
*/
function isIpBlocked(ip) {
if (!ip || ip === 'unknown') return false;
// Check against configured blocked ranges
for (const range of BLOCKED_IP_RANGES) {
if (isIpInRange(ip, range)) {
return true;
}
}
// Block common private/test ranges by default
const privateRanges = [
{ start: '10.0.0.0', end: '10.255.255.255' },
{ start: '172.16.0.0', end: '172.31.255.255' },
{ start: '192.168.0.0', end: '192.168.255.255' },
{ start: '127.0.0.0', end: '127.255.255.255' },
];
for (const range of privateRanges) {
if (isIpInRange(ip, range.start, range.end)) {
return true;
}
}
return false;
}
/**
* Simple IP range check (for IPv4)
*/
function isIpInRange(ip, startOrCidr, end) {
if (!ip) return false;
// Handle CIDR notation
if (startOrCidr.includes('/')) {
const [network, prefix] = startOrCidr.split('/');
const mask = parseInt(prefix, 10);
const ipNum = ipToNumber(ip);
const networkNum = ipToNumber(network);
const maskNum = (0xFFFFFFFF << (32 - mask)) >>> 0;
return (ipNum & maskNum) === (networkNum & maskNum);
}
// Handle start-end range
if (end) {
const ipNum = ipToNumber(ip);
const startNum = ipToNumber(startOrCidr);
const endNum = ipToNumber(end);
return ipNum >= startNum && ipNum <= endNum;
}
return false;
}
/**
* Convert IPv4 address to number
*/
function ipToNumber(ip) {
const parts = ip.split('.');
if (parts.length !== 4) return 0;
return parts.reduce((acc, part) => (acc << 8) + parseInt(part, 10), 0) >>> 0;
}
/**
* Create a device fingerprint from user agent and device info
*/
function createDeviceFingerprint(userAgent, deviceInfo = {}) {
const components = [
userAgent || 'unknown',
deviceInfo.platform || 'unknown',
deviceInfo.model || 'unknown',
deviceInfo.os_version || 'unknown',
];
const fingerprint = components.join('|');
return crypto.createHash('sha256').update(fingerprint).digest('hex').slice(0, 32);
}
/**
* Calculate risk score for a login/refresh attempt
* Returns: { score: number (0-100), reasons: string[], isSuspicious: boolean }
*/
async function calculateRiskScore({
userId,
currentIp,
currentUserAgent,
currentDeviceId,
currentDeviceInfo,
previousIp,
previousUserAgent,
previousDeviceId,
}) {
const reasons = [];
let score = 0;
// Check if IP is blocked
if (isIpBlocked(currentIp)) {
score += 50;
reasons.push('blocked_ip_range');
}
// Check for IP change
if (previousIp && currentIp !== previousIp) {
// Check if IP changed significantly (different subnet)
const ipChanged = !areIpsSimilar(currentIp, previousIp);
if (ipChanged) {
score += 20;
reasons.push('ip_change');
}
}
// Check for device change
if (previousDeviceId && currentDeviceId !== previousDeviceId) {
score += 30;
reasons.push('device_change');
}
// Check for user agent change
if (previousUserAgent && currentUserAgent !== previousUserAgent) {
const uaChanged = !areUserAgentsSimilar(currentUserAgent, previousUserAgent);
if (uaChanged) {
score += 15;
reasons.push('user_agent_change');
}
}
// Check for new device (no previous data)
if (!previousIp && !previousDeviceId) {
// First login is normal, but still track it
reasons.push('first_login');
}
const isSuspicious = score >= 30; // Threshold for suspicious activity
return {
score: Math.min(score, 100),
reasons,
isSuspicious,
};
}
/**
* Check if two IPs are in similar ranges (same /24 subnet)
*/
function areIpsSimilar(ip1, ip2) {
if (!ip1 || !ip2) return false;
const num1 = ipToNumber(ip1);
const num2 = ipToNumber(ip2);
// Same /24 subnet (first 24 bits match)
return (num1 >>> 8) === (num2 >>> 8);
}
/**
* Check if two user agents are similar (basic check)
*/
function areUserAgentsSimilar(ua1, ua2) {
if (!ua1 || !ua2) return false;
// Extract browser/OS from user agent (simplified)
const extractKey = (ua) => {
const lower = ua.toLowerCase();
if (lower.includes('chrome')) return 'chrome';
if (lower.includes('firefox')) return 'firefox';
if (lower.includes('safari')) return 'safari';
if (lower.includes('android')) return 'android';
if (lower.includes('ios')) return 'ios';
return 'unknown';
};
return extractKey(ua1) === extractKey(ua2);
}
/**
* Get previous login/refresh information for a user
*/
async function getPreviousAuthInfo(userId, deviceId) {
try {
// Get most recent successful auth for this user
const result = await db.query(
`SELECT ip_address, user_agent, device_id
FROM auth_audit
WHERE user_id = $1
AND status = 'success'
AND action IN ('login', 'token_refresh')
ORDER BY created_at DESC
LIMIT 1`,
[userId]
);
if (result.rows.length > 0) {
return {
previousIp: result.rows[0].ip_address,
previousUserAgent: result.rows[0].user_agent,
previousDeviceId: result.rows[0].device_id,
};
}
// Fallback: check refresh_tokens for this device
if (deviceId) {
const tokenResult = await db.query(
`SELECT ip_address, user_agent
FROM refresh_tokens
WHERE user_id = $1 AND device_id = $2
ORDER BY created_at DESC
LIMIT 1`,
[userId, deviceId]
);
if (tokenResult.rows.length > 0) {
return {
previousIp: tokenResult.rows[0].ip_address,
previousUserAgent: tokenResult.rows[0].user_agent,
previousDeviceId: deviceId,
};
}
}
return {
previousIp: null,
previousUserAgent: null,
previousDeviceId: null,
};
} catch (err) {
console.error('Error getting previous auth info:', err);
return {
previousIp: null,
previousUserAgent: null,
previousDeviceId: null,
};
}
}
module.exports = {
isIpBlocked,
createDeviceFingerprint,
calculateRiskScore,
getPreviousAuthInfo,
REQUIRE_OTP_ON_SUSPICIOUS_REFRESH,
};