import { Repository } from 'typeorm'; import { IUser } from '../interfaces/entities.interface'; import { BaseSession, BaseAuthAudit } from '../entities'; import { ClientInfo } from '../interfaces/client-info.interface'; import { NAuthConfig } from '../interfaces/config.interface'; import { NAuthLogger } from '../utils/nauth-logger'; import { RiskFactor } from '../enums/risk-factor.enum'; /** * Risk Detection Service * * Analyzes authentication attempts for risk factors by comparing current * context against user's historical behavior (sessions and audit trail). * * **Risk Factors Detected:** * - `new_device`: DeviceId never seen before (check sessions table) * - `new_ip`: IP address never seen before (check sessions + audit) * - `new_country`: Country never seen before (check sessions where ipCountry) * - `impossible_travel`: Geographic distance impossible in time window * - `suspicious_activity`: Recent failed attempts, token reuse, etc. * * **Design Notes:** * - All queries use userId (internal integer ID) for optimal performance * - Queries are optimized with COUNT and LIMIT 1 for existence checks * - Non-blocking: Errors logged but don't throw (graceful degradation) * - Impossible travel detection requires city-level geolocation (optional) * * @example * ```typescript * const riskFactors = await riskDetectionService.detectRiskFactors(user, clientInfo); * // Returns: ['new_device', 'new_country'] * ``` */ export declare class RiskDetectionService { private readonly sessionRepository; private readonly auditRepository; private readonly config; private readonly logger; private readonly trustedDeviceService?; constructor(sessionRepository: Repository, auditRepository: Repository, config: NAuthConfig, logger: NAuthLogger, trustedDeviceService?: { isDeviceTrusted: (deviceToken: string, userId: number) => Promise; } | undefined); /** * Detect risk factors for current authentication attempt * * Compares current context against user's historical behavior to identify * potential security risks. Returns array of detected risk factor strings. * * **Double-Counting Prevention:** * - If `new_country` is detected, `new_ip` is NOT checked (IP is source of country data) * - If `impossible_travel` is detected (city change), `new_ip` is NOT checked * - This prevents double-counting the same underlying risk (location change) * * @param user - User being authenticated * @param clientInfo - Current request context (IP, device, location, etc.) * @returns Array of detected risk factor strings * * @example * ```typescript * const factors = await riskDetectionService.detectRiskFactors(user, clientInfo); * // Returns: ['new_device', 'new_country'] // new_ip excluded if new_country detected * ``` */ detectRiskFactors(user: IUser, clientInfo: ClientInfo): Promise; /** * Check if device has been seen before * * Checks trusted devices first (if available), then sessions table. * If device is trusted, it's not considered "new" even if no sessions exist yet. * * @param userId - Internal user ID (integer) * @param deviceToken - Device token from client * @returns True if device is new (never seen before and not trusted) * @private */ private isNewDevice; /** * Check if user has logged in before (has any previous sessions) * * Used to determine if missing deviceToken should be treated as suspicious. * If user has never logged in before, missing token is expected (first login). * If user has logged in before, missing token is suspicious (incognito, cleared cookies, etc.). * * @param userId - Internal user ID (integer) * @returns True if user has at least one previous session * @private */ private hasUserLoggedInBefore; /** * Normalize IP address for consistent comparison * * Removes port numbers and normalizes IPv6 addresses. * This ensures IPs like "192.168.1.1:8080" and "192.168.1.1" are treated as the same. * * @param ipAddress - IP address to normalize * @returns Normalized IP address * @private */ private normalizeIpAddress; /** * Check if IP address has been seen before * * Queries sessions table first (faster), then audit table for older data. * Uses normalized IP addresses for consistent comparison. * * @param userId - Internal user ID (integer) * @param ipAddress - IP address to check (should already be normalized) * @returns True if IP is new (never seen before) * @private */ private isNewIp; /** * Check if country has been seen before * * Queries sessions table for any past session from this country. * **Optimization:** Uses 1-2 queries instead of 3 by checking country existence first * (most likely to short-circuit), then verifying country data availability if needed. * * **Important:** * - On first login (no previous sessions), returns false (no history to compare) * - If sessions exist but none have ipCountry data (null), returns false (can't determine) * - Only flags as new if we have sessions with country data AND none match * * @param userId - Internal user ID (integer) * @param country - Country code to check (e.g., 'US', 'GB') * @returns True if country is new (never seen before), false on first login, if no country data, or if country seen before * @private */ private isNewCountry; /** * Detect impossible travel * * Calculates if geographic distance between last location and current * location is impossible given time elapsed. * * **Algorithm:** * 1. Get last login location (ipCountry, ipCity, coordinates) and createdAt * 2. Calculate distance using coordinates (Haversine) or heuristics (fallback) * 3. Calculate max possible speed (distance / time) * 4. If speed > threshold (default 900 km/h), flag as impossible * * **Edge Cases Handled:** * - No previous location data → false (benefit of doubt) * - Same location → false (not travel) * - Missing city but country changed → true (suspicious, different country in short time) * - Missing coordinates → use heuristic distance estimates * * @param userId - Internal user ID (integer) * @param currentInfo - Current client info with location * @returns True if travel is impossible * @private */ private detectImpossibleTravel; /** * Calculate distance between two points using Haversine formula * * Accurate distance calculation using geographic coordinates (latitude/longitude). * This is the preferred method when coordinates are available. * * **Haversine Formula:** * - Accounts for Earth's spherical shape * - Returns great-circle distance in kilometers * - Accuracy: ~0.5% for most distances * * @param lat1 - Latitude of first point (degrees) * @param lon1 - Longitude of first point (degrees) * @param lat2 - Latitude of second point (degrees) * @param lon2 - Longitude of second point (degrees) * @returns Distance in kilometers (accurate) * @private */ private calculateHaversineDistance; /** * Convert degrees to radians * * @param degrees - Angle in degrees * @returns Angle in radians * @private */ private toRadians; /** * Calculate distance between two cities (heuristic fallback) * * Heuristic-based implementation for estimating travel distance when * precise coordinates are not available. Uses continent and regional * groupings for realistic estimates. * * **Fallback approach:** * - Same city: 0 km * - Same country, different city: 500 km (average domestic travel) * - Different country, same continent: 1,500 km (regional travel) * - Different continent (intercontinental): 8,000 km (long-haul flight) * - Missing city data: use country-level comparison * * @param city1 - First city name (nullable) * @param country1 - First country code (ISO 2-letter) * @param city2 - Second city name (nullable) * @param country2 - Second country code (ISO 2-letter) * @returns Distance in kilometers (estimated) * @private */ private calculateDistance; /** * Get continent for a country code * * Maps ISO 2-letter country codes to continents for distance estimation. * This is used to differentiate between regional and intercontinental travel. * * @param countryCode - ISO 2-letter country code * @returns Continent name * @private */ private getContinent; /** * Detect suspicious activity patterns * * Checks for: * - Recent failed login attempts (last 1 hour) * - Token reuse detected (SUSPICIOUS_ACTIVITY audit events) * - Multiple MFA failures * - Account lockout attempts * * @param userId - Internal user ID (integer) * @returns True if suspicious activity detected * @private */ private detectSuspiciousActivity; } //# sourceMappingURL=risk-detection.service.d.ts.map