get_authorization_header(); // Must be Bearer token format return $auth_header && strpos($auth_header, 'Bearer ') === 0; } public function authenticate(): ?int { if (!$this->can_authenticate()) { return null; } $token = $this->extract_bearer_token(); if (!$token) { throw new \Exception('Invalid Bearer token format'); } // Validate token and get associated user $user_id = $this->validate_token($token); if (!$user_id) { throw new \Exception('Invalid or expired token'); } // Verify user still exists and is active $user = get_user_by('ID', $user_id); if (!$user || !$this->is_user_active($user)) { throw new \Exception('User account is inactive or deleted'); } return (int) $user_id; } public function validate_credentials(): bool { if (!$this->can_authenticate()) { return false; } $token = $this->extract_bearer_token(); return $token && $this->validate_token($token) !== null; } public function get_priority(): int { return 10; // Standard priority for OAuth2 } public function requires_https(): bool { return true; // OAuth2 Bearer tokens should always require HTTPS in production } public function get_allowed_methods(): array { return ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']; } public function get_response_headers(): array { return [ 'WWW-Authenticate' => 'Bearer realm="WordPress REST API"', ]; } /** * Get Authorization header from request */ private function get_authorization_header(): ?string { $auth_header = null; // Try Apache/Nginx style if (isset($_SERVER['HTTP_AUTHORIZATION'])) { $auth_header = $_SERVER['HTTP_AUTHORIZATION']; } // Try alternative header names elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { $auth_header = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; } // Try getallheaders() if available elseif (function_exists('getallheaders')) { $headers = getallheaders(); if (isset($headers['Authorization'])) { $auth_header = $headers['Authorization']; } elseif (isset($headers['authorization'])) { $auth_header = $headers['authorization']; } } // 🔐 SECURITY: Validate authorization header format to prevent injection if ($auth_header !== null) { // Only allow Bearer tokens with valid base64-like characters if (preg_match('/^Bearer\s+([A-Za-z0-9+\/=._-]+)$/i', $auth_header, $matches)) { return $auth_header; } // Log suspicious authorization header attempts if (defined('WP_DEBUG') && WP_DEBUG) { error_log('[OAuth2 Security] Invalid authorization header format: ' . substr($auth_header, 0, 50)); } return null; } return null; } /** * Extract Bearer token from Authorization header */ private function extract_bearer_token(): ?string { $auth_header = $this->get_authorization_header(); if (!$auth_header || strpos($auth_header, 'Bearer ') !== 0) { return null; } $token = substr($auth_header, 7); // Remove "Bearer " prefix $token = trim($token); // Basic token format validation if (empty($token) || strlen($token) < 16) { return null; } // Only allow alphanumeric characters and common token characters if (!preg_match('/^[a-zA-Z0-9._-]+$/', $token)) { return null; } return $token; } /** * Validate token and return associated user ID */ private function validate_token(string $token): ?int { // Use OAuth2Server for database-backed token validation $oauth2_server = new OAuth2Server([]); $token_data = $oauth2_server->validate_access_token($token); if ($token_data) { return (int) $token_data['user_id']; } // Fallback: Check against stored test token for backward compatibility $test_token = get_option('wp_oauth2_poc_test_token'); if ($token === $test_token) { // Return admin user for test token $admin_users = get_users(['role' => 'administrator', 'number' => 1]); if (!empty($admin_users)) { return $admin_users[0]->ID; } } // Legacy PoC token storage check $stored_tokens = get_option(self::TOKEN_STORAGE_OPTION, []); foreach ($stored_tokens as $stored_token) { if ($stored_token['token'] === $token) { // Check expiration if (time() > $stored_token['expires']) { continue; // Token expired } return (int) $stored_token['user_id']; } } return null; } /** * Check if user account is active */ private function is_user_active(\WP_User $user): bool { // Check if user account is active if (isset($user->user_status) && $user->user_status != 0) { return false; } // Check if user has required capabilities if (!$user->exists()) { return false; } // Additional checks can be added here return true; } /** * Generate a new OAuth2 access token for a user (for PoC demonstration) */ public static function generate_token_for_user(int $user_id, int $expires_in = 3600): string { $token = bin2hex(random_bytes(32)); $expires = time() + $expires_in; $stored_tokens = get_option(self::TOKEN_STORAGE_OPTION, []); $stored_tokens[] = [ 'token' => $token, 'user_id' => $user_id, 'expires' => $expires, 'created' => time(), 'scope' => 'basic', ]; update_option(self::TOKEN_STORAGE_OPTION, $stored_tokens); return $token; } /** * Revoke a token */ public static function revoke_token(string $token): bool { $stored_tokens = get_option(self::TOKEN_STORAGE_OPTION, []); $stored_tokens = array_filter($stored_tokens, function($stored_token) use ($token) { return $stored_token['token'] !== $token; }); update_option(self::TOKEN_STORAGE_OPTION, array_values($stored_tokens)); return true; } /** * Clean up expired tokens */ public static function cleanup_expired_tokens(): int { $stored_tokens = get_option(self::TOKEN_STORAGE_OPTION, []); $current_time = time(); $removed_count = 0; $active_tokens = array_filter($stored_tokens, function($token) use ($current_time, &$removed_count) { if ($current_time > $token['expires']) { $removed_count++; return false; } return true; }); update_option(self::TOKEN_STORAGE_OPTION, array_values($active_tokens)); return $removed_count; } }