Jake Vanderwerf
2026-08-26 ccf5e61a075c35d24befc175994b5cb8957ca322
inc/rest/routes/LoginRoutes.php
@@ -159,15 +159,8 @@
         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(),
@@ -175,21 +168,6 @@
         );
      }
      // 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);
@@ -201,6 +179,40 @@
      ]);
   }
   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
    */
@@ -506,87 +518,6 @@
      ]);
   }
   /************************************************************
    * 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
    **************************************************************/