/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ * @link https://descomplicar.pt * @since 1.0.0 */ namespace Care_API\Services; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class Session_Service * * Session management and security monitoring for Care API * * @since 1.0.0 */ class Session_Service { /** * Maximum concurrent sessions per user * * @var int */ private static $max_concurrent_sessions = 3; /** * Session timeout (in seconds) - 30 minutes * * @var int */ private static $session_timeout = 1800; /** * Maximum failed login attempts * * @var int */ private static $max_failed_attempts = 5; /** * Lockout duration (in seconds) - 15 minutes * * @var int */ private static $lockout_duration = 900; /** * Initialize the session service * * @since 1.0.0 */ public static function init() { // Hook into authentication events add_action( 'kivicare_user_authenticated', array( self::class, 'on_user_authenticated' ), 10, 2 ); add_action( 'kivicare_user_logout', array( self::class, 'on_user_logout' ), 10, 1 ); add_action( 'kivicare_failed_login', array( self::class, 'on_failed_login' ), 10, 1 ); // Cleanup expired sessions add_action( 'kivicare_cleanup_sessions', array( self::class, 'cleanup_expired_sessions' ) ); // Schedule cleanup if not already scheduled if ( ! wp_next_scheduled( 'kivicare_cleanup_sessions' ) ) { wp_schedule_event( time(), 'hourly', 'kivicare_cleanup_sessions' ); } // Monitor session activity add_action( 'init', array( self::class, 'monitor_session_activity' ) ); } /** * Create new session for user * * @param int $user_id User ID * @param string $ip_address IP address * @param string $user_agent User agent * @return string Session ID * @since 1.0.0 */ public static function create_session( $user_id, $ip_address, $user_agent ) { // Generate unique session ID $session_id = wp_generate_uuid4(); // Check concurrent session limit self::enforce_concurrent_session_limit( $user_id ); // Create session data $session_data = array( 'session_id' => $session_id, 'user_id' => $user_id, 'ip_address' => $ip_address, 'user_agent' => $user_agent, 'created_at' => current_time( 'mysql' ), 'last_activity' => current_time( 'mysql' ), 'expires_at' => date( 'Y-m-d H:i:s', time() + self::$session_timeout ), 'is_active' => 1 ); // Store session in database self::store_session( $session_data ); // Update user session metadata self::update_user_session_meta( $user_id, $session_id, $ip_address ); return $session_id; } /** * Validate session * * @param string $session_id Session ID * @param int $user_id User ID * @return bool|array Session data or false if invalid * @since 1.0.0 */ public static function validate_session( $session_id, $user_id ) { $session = self::get_session( $session_id ); if ( ! $session ) { return false; } // Check if session belongs to user if ( (int) $session['user_id'] !== $user_id ) { return false; } // Check if session is active if ( ! $session['is_active'] ) { return false; } // Check if session is expired if ( strtotime( $session['expires_at'] ) < time() ) { self::expire_session( $session_id ); return false; } // Check for session timeout based on last activity $last_activity = strtotime( $session['last_activity'] ); if ( ( time() - $last_activity ) > self::$session_timeout ) { self::expire_session( $session_id ); return false; } return $session; } /** * Update session activity * * @param string $session_id Session ID * @return bool Success status * @since 1.0.0 */ public static function update_session_activity( $session_id ) { global $wpdb; $result = $wpdb->update( $wpdb->prefix . 'kivicare_sessions', array( 'last_activity' => current_time( 'mysql' ), 'expires_at' => date( 'Y-m-d H:i:s', time() + self::$session_timeout ) ), array( 'session_id' => $session_id ), array( '%s', '%s' ), array( '%s' ) ); return $result !== false; } /** * Expire session * * @param string $session_id Session ID * @return bool Success status * @since 1.0.0 */ public static function expire_session( $session_id ) { global $wpdb; $result = $wpdb->update( $wpdb->prefix . 'kivicare_sessions', array( 'is_active' => 0, 'ended_at' => current_time( 'mysql' ) ), array( 'session_id' => $session_id ), array( '%d', '%s' ), array( '%s' ) ); return $result !== false; } /** * Expire all sessions for user * * @param int $user_id User ID * @return bool Success status * @since 1.0.0 */ public static function expire_user_sessions( $user_id ) { global $wpdb; $result = $wpdb->update( $wpdb->prefix . 'kivicare_sessions', array( 'is_active' => 0, 'ended_at' => current_time( 'mysql' ) ), array( 'user_id' => $user_id, 'is_active' => 1 ), array( '%d', '%s' ), array( '%d', '%d' ) ); // Clear user session metadata delete_user_meta( $user_id, 'kivicare_current_session' ); delete_user_meta( $user_id, 'kivicare_session_ip' ); return $result !== false; } /** * Get active sessions for user * * @param int $user_id User ID * @return array Array of active sessions * @since 1.0.0 */ public static function get_user_sessions( $user_id ) { global $wpdb; $sessions = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}kivicare_sessions WHERE user_id = %d AND is_active = 1 AND expires_at > NOW() ORDER BY last_activity DESC", $user_id ), ARRAY_A ); return array_map( array( self::class, 'format_session_data' ), $sessions ); } /** * Check if user account is locked * * @param int|string $user_identifier User ID or username * @return bool True if account is locked * @since 1.0.0 */ public static function is_account_locked( $user_identifier ) { if ( is_numeric( $user_identifier ) ) { $user = get_user_by( 'id', $user_identifier ); } else { $user = get_user_by( 'login', $user_identifier ); if ( ! $user && is_email( $user_identifier ) ) { $user = get_user_by( 'email', $user_identifier ); } } if ( ! $user ) { return false; } $lockout_time = get_user_meta( $user->ID, 'kivicare_lockout_time', true ); if ( ! $lockout_time ) { return false; } // Check if lockout has expired if ( time() > $lockout_time ) { delete_user_meta( $user->ID, 'kivicare_lockout_time' ); delete_user_meta( $user->ID, 'kivicare_failed_attempts' ); return false; } return true; } /** * Get remaining lockout time * * @param int $user_id User ID * @return int Remaining lockout time in seconds * @since 1.0.0 */ public static function get_lockout_remaining_time( $user_id ) { $lockout_time = get_user_meta( $user_id, 'kivicare_lockout_time', true ); if ( ! $lockout_time ) { return 0; } $remaining = $lockout_time - time(); return max( 0, $remaining ); } /** * Get session statistics for user * * @param int $user_id User ID * @return array Session statistics * @since 1.0.0 */ public static function get_user_session_stats( $user_id ) { global $wpdb; $stats = array( 'active_sessions' => 0, 'total_sessions_today' => 0, 'total_sessions_this_month' => 0, 'last_login' => null, 'last_ip' => null, 'failed_attempts_today' => 0, 'is_locked' => false, 'lockout_remaining' => 0 ); // Active sessions $stats['active_sessions'] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_sessions WHERE user_id = %d AND is_active = 1 AND expires_at > NOW()", $user_id ) ); // Sessions today $stats['total_sessions_today'] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_sessions WHERE user_id = %d AND DATE(created_at) = CURDATE()", $user_id ) ); // Sessions this month $stats['total_sessions_this_month'] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_sessions WHERE user_id = %d AND MONTH(created_at) = MONTH(CURDATE()) AND YEAR(created_at) = YEAR(CURDATE())", $user_id ) ); // Last login info $last_session = $wpdb->get_row( $wpdb->prepare( "SELECT created_at, ip_address FROM {$wpdb->prefix}kivicare_sessions WHERE user_id = %d ORDER BY created_at DESC LIMIT 1", $user_id ), ARRAY_A ); if ( $last_session ) { $stats['last_login'] = $last_session['created_at']; $stats['last_ip'] = $last_session['ip_address']; } // Failed attempts today $stats['failed_attempts_today'] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_failed_logins WHERE user_id = %d AND DATE(attempted_at) = CURDATE()", $user_id ) ); // Lockout status $stats['is_locked'] = self::is_account_locked( $user_id ); $stats['lockout_remaining'] = self::get_lockout_remaining_time( $user_id ); return $stats; } /** * Monitor session activity for security * * @since 1.0.0 */ public static function monitor_session_activity() { // Only monitor for authenticated API requests if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) { return; } $user_id = get_current_user_id(); if ( ! $user_id ) { return; } // Get current session from auth header or meta $session_id = self::get_current_session_id( $user_id ); if ( $session_id ) { // Update session activity self::update_session_activity( $session_id ); // Check for suspicious activity self::detect_suspicious_activity( $user_id, $session_id ); } } /** * Get current session ID for user * * @param int $user_id User ID * @return string|null Session ID or null * @since 1.0.0 */ private static function get_current_session_id( $user_id ) { // Try to get from JWT token first (would need to be added to JWT payload) // For now, get from user meta (set during authentication) return get_user_meta( $user_id, 'kivicare_current_session', true ); } /** * Detect suspicious session activity * * @param int $user_id User ID * @param string $session_id Session ID * @since 1.0.0 */ private static function detect_suspicious_activity( $user_id, $session_id ) { $session = self::get_session( $session_id ); if ( ! $session ) { return; } $current_ip = self::get_client_ip(); $session_ip = $session['ip_address']; // Check for IP address change if ( $current_ip !== $session_ip ) { self::log_security_event( $user_id, 'ip_change', array( 'session_id' => $session_id, 'original_ip' => $session_ip, 'new_ip' => $current_ip ) ); // Optionally expire session or require re-authentication if ( apply_filters( 'kivicare_expire_on_ip_change', false ) ) { self::expire_session( $session_id ); } } // Check for unusual activity patterns self::check_activity_patterns( $user_id, $session_id ); } /** * Check for unusual activity patterns * * @param int $user_id User ID * @param string $session_id Session ID * @since 1.0.0 */ private static function check_activity_patterns( $user_id, $session_id ) { // This could be extended to check: // - Rapid API calls (possible bot activity) // - Access to unusual resources // - Concurrent sessions from different locations // - Time-based anomalies (access at unusual hours) do_action( 'kivicare_check_activity_patterns', $user_id, $session_id ); } /** * Handle user authentication event * * @param int $user_id User ID * @param array $context Authentication context * @since 1.0.0 */ public static function on_user_authenticated( $user_id, $context ) { $ip_address = $context['ip_address'] ?? self::get_client_ip(); $user_agent = $context['user_agent'] ?? ( $_SERVER['HTTP_USER_AGENT'] ?? '' ); // Create new session $session_id = self::create_session( $user_id, $ip_address, $user_agent ); // Log successful authentication self::log_security_event( $user_id, 'login', array( 'session_id' => $session_id, 'ip_address' => $ip_address, 'user_agent' => $user_agent ) ); // Clear failed login attempts delete_user_meta( $user_id, 'kivicare_failed_attempts' ); delete_user_meta( $user_id, 'kivicare_lockout_time' ); } /** * Handle user logout event * * @param int $user_id User ID * @since 1.0.0 */ public static function on_user_logout( $user_id ) { $session_id = get_user_meta( $user_id, 'kivicare_current_session', true ); if ( $session_id ) { self::expire_session( $session_id ); } // Log logout self::log_security_event( $user_id, 'logout', array( 'session_id' => $session_id ) ); } /** * Handle failed login event * * @param array $context Failed login context * @since 1.0.0 */ public static function on_failed_login( $context ) { $user_id = $context['user_id'] ?? null; $username = $context['username'] ?? ''; $ip_address = $context['ip_address'] ?? self::get_client_ip(); // Log failed attempt self::log_failed_login_attempt( $user_id, $username, $ip_address ); if ( $user_id ) { // Increment failed attempts counter $failed_attempts = (int) get_user_meta( $user_id, 'kivicare_failed_attempts', true ) + 1; update_user_meta( $user_id, 'kivicare_failed_attempts', $failed_attempts ); // Check if account should be locked if ( $failed_attempts >= self::$max_failed_attempts ) { self::lock_account( $user_id ); } } // Log security event self::log_security_event( $user_id, 'failed_login', $context ); } /** * Lock user account * * @param int $user_id User ID * @since 1.0.0 */ private static function lock_account( $user_id ) { $lockout_time = time() + self::$lockout_duration; update_user_meta( $user_id, 'kivicare_lockout_time', $lockout_time ); // Expire all active sessions self::expire_user_sessions( $user_id ); // Log security event self::log_security_event( $user_id, 'account_locked', array( 'lockout_duration' => self::$lockout_duration, 'lockout_until' => date( 'Y-m-d H:i:s', $lockout_time ) ) ); // Send notification (could be extended) do_action( 'kivicare_account_locked', $user_id ); } /** * Enforce concurrent session limit * * @param int $user_id User ID * @since 1.0.0 */ private static function enforce_concurrent_session_limit( $user_id ) { global $wpdb; // Get active sessions count $active_sessions = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_sessions WHERE user_id = %d AND is_active = 1 AND expires_at > NOW()", $user_id ) ); // If at limit, expire oldest session if ( $active_sessions >= self::$max_concurrent_sessions ) { $oldest_session = $wpdb->get_var( $wpdb->prepare( "SELECT session_id FROM {$wpdb->prefix}kivicare_sessions WHERE user_id = %d AND is_active = 1 AND expires_at > NOW() ORDER BY last_activity ASC LIMIT 1", $user_id ) ); if ( $oldest_session ) { self::expire_session( $oldest_session ); } } } /** * Store session in database * * @param array $session_data Session data * @return bool Success status * @since 1.0.0 */ private static function store_session( $session_data ) { global $wpdb; // Create table if it doesn't exist self::create_sessions_table(); $result = $wpdb->insert( $wpdb->prefix . 'kivicare_sessions', $session_data, array( '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%d' ) ); return $result !== false; } /** * Get session from database * * @param string $session_id Session ID * @return array|null Session data or null * @since 1.0.0 */ private static function get_session( $session_id ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}kivicare_sessions WHERE session_id = %s", $session_id ), ARRAY_A ); } /** * Update user session metadata * * @param int $user_id User ID * @param string $session_id Session ID * @param string $ip_address IP address * @since 1.0.0 */ private static function update_user_session_meta( $user_id, $session_id, $ip_address ) { update_user_meta( $user_id, 'kivicare_current_session', $session_id ); update_user_meta( $user_id, 'kivicare_session_ip', $ip_address ); update_user_meta( $user_id, 'kivicare_last_activity', current_time( 'mysql' ) ); } /** * Log failed login attempt * * @param int|null $user_id User ID (if found) * @param string $username Username attempted * @param string $ip_address IP address * @since 1.0.0 */ private static function log_failed_login_attempt( $user_id, $username, $ip_address ) { global $wpdb; // Create table if it doesn't exist self::create_failed_logins_table(); $wpdb->insert( $wpdb->prefix . 'kivicare_failed_logins', array( 'user_id' => $user_id, 'username' => $username, 'ip_address' => $ip_address, 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'attempted_at' => current_time( 'mysql' ) ), array( '%d', '%s', '%s', '%s', '%s' ) ); } /** * Log security event * * @param int $user_id User ID * @param string $event Event type * @param array $data Event data * @since 1.0.0 */ private static function log_security_event( $user_id, $event, $data = array() ) { global $wpdb; // Create table if it doesn't exist self::create_security_log_table(); $wpdb->insert( $wpdb->prefix . 'kivicare_security_log', array( 'user_id' => $user_id, 'event_type' => $event, 'event_data' => wp_json_encode( $data ), 'ip_address' => self::get_client_ip(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'created_at' => current_time( 'mysql' ) ), array( '%d', '%s', '%s', '%s', '%s', '%s' ) ); } /** * Format session data for API response * * @param array $session_data Raw session data * @return array Formatted session data * @since 1.0.0 */ private static function format_session_data( $session_data ) { return array( 'session_id' => $session_data['session_id'], 'ip_address' => $session_data['ip_address'], 'user_agent' => $session_data['user_agent'], 'created_at' => $session_data['created_at'], 'last_activity' => $session_data['last_activity'], 'expires_at' => $session_data['expires_at'], 'is_current' => get_user_meta( $session_data['user_id'], 'kivicare_current_session', true ) === $session_data['session_id'] ); } /** * Cleanup expired sessions * * @since 1.0.0 */ public static function cleanup_expired_sessions() { global $wpdb; // Delete expired sessions $wpdb->query( "DELETE FROM {$wpdb->prefix}kivicare_sessions WHERE expires_at < NOW() OR (is_active = 0 AND ended_at < DATE_SUB(NOW(), INTERVAL 30 DAY))" ); // Clean up old failed login attempts $wpdb->query( "DELETE FROM {$wpdb->prefix}kivicare_failed_logins WHERE attempted_at < DATE_SUB(NOW(), INTERVAL 7 DAY)" ); // Clean up old security log entries (keep 90 days) $wpdb->query( "DELETE FROM {$wpdb->prefix}kivicare_security_log WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)" ); } /** * Get client IP address * * @return string IP address * @since 1.0.0 */ private static function get_client_ip() { $ip_keys = array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR' ); foreach ( $ip_keys as $key ) { if ( ! empty( $_SERVER[ $key ] ) ) { $ip = $_SERVER[ $key ]; if ( strpos( $ip, ',' ) !== false ) { $ip = explode( ',', $ip )[0]; } $ip = trim( $ip ); if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { return $ip; } } } return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } /** * Create sessions table * * @since 1.0.0 */ private static function create_sessions_table() { global $wpdb; $table_name = $wpdb->prefix . 'kivicare_sessions'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS {$table_name} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, session_id varchar(36) NOT NULL, user_id bigint(20) unsigned NOT NULL, ip_address varchar(45) NOT NULL, user_agent text NOT NULL, created_at datetime NOT NULL, last_activity datetime NOT NULL, expires_at datetime NOT NULL, ended_at datetime NULL, is_active tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY session_id (session_id), KEY user_id (user_id), KEY expires_at (expires_at), KEY is_active (is_active) ) {$charset_collate};"; require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); dbDelta( $sql ); } /** * Create failed logins table * * @since 1.0.0 */ private static function create_failed_logins_table() { global $wpdb; $table_name = $wpdb->prefix . 'kivicare_failed_logins'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS {$table_name} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned NULL, username varchar(60) NOT NULL, ip_address varchar(45) NOT NULL, user_agent text NOT NULL, attempted_at datetime NOT NULL, PRIMARY KEY (id), KEY user_id (user_id), KEY ip_address (ip_address), KEY attempted_at (attempted_at) ) {$charset_collate};"; require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); dbDelta( $sql ); } /** * Create security log table * * @since 1.0.0 */ private static function create_security_log_table() { global $wpdb; $table_name = $wpdb->prefix . 'kivicare_security_log'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS {$table_name} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned NULL, event_type varchar(50) NOT NULL, event_data longtext NULL, ip_address varchar(45) NOT NULL, user_agent text NOT NULL, created_at datetime NOT NULL, PRIMARY KEY (id), KEY user_id (user_id), KEY event_type (event_type), KEY created_at (created_at) ) {$charset_collate};"; require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); dbDelta( $sql ); } }