| | |
| | | error_log('[handleLogin]Turnstile succeeded'); |
| | | } |
| | | |
| | | // Attempt authentication |
| | | $user = wp_authenticate($email, $password); |
| | | |
| | | $user = $this->login($email, $password, $remember); |
| | | if (is_wp_error($user)) { |
| | | $this->auditLog('login_failed', [ |
| | | 'email' => $email, |
| | | 'error' => $user->get_error_code(), |
| | | ]); |
| | | |
| | | return $this->error( |
| | | $this->getLoginErrorMessage($user), |
| | | $user->get_error_code(), |
| | |
| | | ); |
| | | } |
| | | |
| | | // Set auth cookie |
| | | wp_clear_auth_cookie(); |
| | | wp_set_current_user($user->ID); |
| | | wp_set_auth_cookie($user->ID, $remember); |
| | | |
| | | // Store session fingerprint |
| | | $this->storeSessionFingerprint($user->ID, $request); |
| | | |
| | | do_action('wp_login', $user->user_login, $user); |
| | | |
| | | $this->auditLog('login_success', [ |
| | | 'user_id' => $user->ID, |
| | | 'email' => $email, |
| | | ]); |
| | | |
| | | // Get redirect URL |
| | | $redirect = $this->getRedirectUrl($user, $redirect_to); |
| | | |
| | |
| | | ]); |
| | | } |
| | | |
| | | public function login(string $email, string $password, bool $remember, ?WP_REST_Request $request = null):WP_User|WP_Error |
| | | { |
| | | // Attempt authentication |
| | | $user = wp_authenticate($email, $password); |
| | | |
| | | if (is_wp_error($user)) { |
| | | $this->auditLog('login_failed', [ |
| | | 'email' => $email, |
| | | 'error' => $user->get_error_code(), |
| | | ]); |
| | | |
| | | return $user; |
| | | } |
| | | |
| | | // Set auth cookie |
| | | wp_clear_auth_cookie(); |
| | | wp_set_current_user($user->ID); |
| | | wp_set_auth_cookie($user->ID, $remember); |
| | | |
| | | // Store session fingerprint |
| | | if ($request) { |
| | | $this->storeSessionFingerprint($user->ID, $request); |
| | | } |
| | | |
| | | |
| | | do_action('wp_login', $user->user_login, $user); |
| | | |
| | | $this->auditLog('login_success', [ |
| | | 'user_id' => $user->ID, |
| | | 'email' => $email, |
| | | ]); |
| | | return $user; |
| | | } |
| | | |
| | | /** |
| | | * Handle user registration |
| | | */ |
| | |
| | | ]); |
| | | } |
| | | |
| | | /************************************************************ |
| | | * SESSION FINGERPRINTING |
| | | * |
| | | * Detects session hijacking by validating that session hasn't |
| | | * moved to a different device/network. Uses IP class (not full IP) |
| | | * to allow mobile network changes without breaking sessions. |
| | | ************************************************************/ |
| | | |
| | | /** |
| | | * Store session fingerprint for hijacking detection |
| | | */ |
| | | protected function storeSessionFingerprint(int $user_id, WP_REST_Request $request): void |
| | | { |
| | | if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) { |
| | | return; |
| | | } |
| | | |
| | | $fingerprint = $this->generateSessionFingerprint($request); |
| | | update_user_meta($user_id, BASE . 'session_fingerprint', $fingerprint); |
| | | update_user_meta($user_id, BASE . 'session_timestamp', time()); |
| | | } |
| | | |
| | | /** |
| | | * Generate session fingerprint for hijacking detection |
| | | */ |
| | | protected function generateSessionFingerprint(WP_REST_Request $request): string |
| | | { |
| | | return hash('sha256', implode('|', [ |
| | | $request->get_header('User-Agent') ?? '', |
| | | // Use IP class instead of full IP to allow for mobile network changes |
| | | $this->getIPClass( |
| | | $request->get_header('X-Forwarded-For') |
| | | ?: $request->get_header('X-Real-IP') |
| | | ?: $_SERVER['REMOTE_ADDR'] ?? '' |
| | | ) |
| | | ])); |
| | | } |
| | | |
| | | /** |
| | | * Get IP class (first 3 octets) for session validation |
| | | * Allows for minor IP changes (common with mobile networks) |
| | | */ |
| | | protected function getIPClass(string $ip): string |
| | | { |
| | | $parts = explode('.', $ip); |
| | | return implode('.', array_slice($parts, 0, 3)); |
| | | } |
| | | |
| | | /** |
| | | * Validate session fingerprint against stored value |
| | | */ |
| | | protected function validateSessionFingerprint(int $user_id, WP_REST_Request $request): bool |
| | | { |
| | | // Only enforce if enabled in config |
| | | if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) { |
| | | return true; |
| | | } |
| | | |
| | | $stored = get_user_meta($user_id, BASE . 'session_fingerprint', true); |
| | | $current = $this->generateSessionFingerprint($request); |
| | | |
| | | if (empty($stored)) { |
| | | // First request - store fingerprint |
| | | update_user_meta($user_id, BASE . 'session_fingerprint', $current); |
| | | update_user_meta($user_id, BASE . 'session_timestamp', time()); |
| | | return true; |
| | | } |
| | | |
| | | // Compare using timing-safe comparison |
| | | return hash_equals($stored, $current); |
| | | } |
| | | |
| | | /** |
| | | * Clear session fingerprint (call on logout) |
| | | */ |
| | | protected function clearSessionFingerprint(int $user_id): void |
| | | { |
| | | delete_user_meta($user_id, BASE . 'session_fingerprint'); |
| | | delete_user_meta($user_id, BASE . 'session_timestamp'); |
| | | } |
| | | |
| | | /************************************************************** |
| | | * HELPERS |
| | | **************************************************************/ |