mcp-1.x-dev/src/Authentication/Provider/McpAuthProvider.php

src/Authentication/Provider/McpAuthProvider.php
<?php

namespace Drupal\mcp\Authentication\Provider;

use Drupal\mcp\Exception\McpAuthException;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Flood\FloodInterface;
use Drupal\mcp\Config\McpSettings;
use Drupal\mcp\PageCache\DisallowMcpAuthRequests;
use Drupal\user\UserAuthenticationInterface;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Drupal\user\UserAuthInterface;

/**
 * MCP Authentication Provider.
 */
class McpAuthProvider implements AuthenticationProviderInterface {

  public function __construct(
    private readonly McpSettings $mcpSettings,
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly UserAuthInterface|UserAuthenticationInterface $userAuth,
    private readonly FloodInterface $flood,
    private readonly ConfigFactoryInterface $configFactory,
    private readonly DisallowMcpAuthRequests $disallowMcpAuthRequests,
  ) {
    if (!$userAuth instanceof UserAuthenticationInterface) {
      @trigger_error(
        'The $user_auth parameter implementing UserAuthInterface is deprecated in drupal:10.3.0 and will be removed in drupal:12.0.0. Implement UserAuthenticationInterface instead. See https://www.drupal.org/node/3411040'
      );
    }
  }

  /**
   * {@inheritDoc}
   */
  public function applies(Request $request) {
    return $this->disallowMcpAuthRequests->isMcpRequest($request);
  }

  /**
   * Authenticates the incoming request based on the configured auth methods.
   *
   * Flood protection: Is similar to the
   * Drupal\basic_auth\Authentication\Provider.
   *
   * Refactor it when:
   * https://www.drupal.org/project/drupal/issues/2431357 merged.
   */
  public function authenticate(Request $request) {
    $flood_config = $this->configFactory->get('user.flood');
    if (!$this->flood->isAllowed(
      'mcp_auth.failed_login_ip', $flood_config->get('ip_limit'),
      $flood_config->get('ip_window')
    )
    ) {
      $this->flood->register(
        'mcp_auth.failed_login_ip', $flood_config->get('ip_window')
      );
      throw new McpAuthException(
        "Too many failed login attempts (IP threshold reached)."
      );
    }

    $authHeader = $request->headers->get('Authorization');
    if (empty($authHeader) || stripos($authHeader, 'Basic ') !== 0) {
      throw new McpAuthException(
        "Missing or invalid Authorization header."
      );
    }

    $encoded = trim(substr($authHeader, 6));
    $decoded = base64_decode($encoded);
    if ($decoded === FALSE) {
      throw new McpAuthException(
        "Invalid base64 encoded credentials."
      );
    }

    if (str_contains($decoded, ':')) {
      $result = $this->authenticateBasic($request, $decoded, $flood_config);
    }
    else {
      $result = $this->authenticateToken($request, $decoded, $flood_config);
    }

    if ($result === NULL) {
      $this->flood->register(
        'mcp_auth.failed_login_ip', $flood_config->get('ip_window')
      );
      throw new McpAuthException(
        "Authentication failed: Invalid credentials."
      );
    }

    return $result;
  }

  /**
   * Authenticates the request using basic auth.
   */
  private function authenticateBasic(
    Request $request,
    string $decoded,
    $flood_config,
  ) {
    $auth_settings = $this->mcpSettings->getAuthSettings();
    if (empty($auth_settings['enable_basic_auth'])) {
      return NULL;
    }
    [$username, $password] = explode(':', $decoded, 2);
    $account = FALSE;
    if ($this->userAuth instanceof UserAuthenticationInterface) {
      $lookup = $this->userAuth->lookupAccount($username);
      if ($lookup) {
        $account = $lookup;
      }
    }
    else {
      $accounts = $this->entityTypeManager->getStorage('user')
        ->loadByProperties(['name' => $username]);
      $account = reset($accounts);

      // If not found and username contains @, try loading by email.
      // This is useful for sites where users log in with email addresses.
      if (!$account && str_contains($username, '@')) {
        $accounts = $this->entityTypeManager->getStorage('user')
          ->loadByProperties(['mail' => $username]);

        $account = reset($accounts);
      }
    }
    if ($account && !$account->isBlocked() && $account->isActive()) {
      $identifier = $flood_config->get('uid_only') ? $account->id()
        : $account->id() . '-' . $request->getClientIP();
      if ($this->flood->isAllowed(
        'mcp_auth.failed_login_user', $flood_config->get('user_limit'),
        $flood_config->get('user_window'), $identifier
      )
      ) {
        $authenticated = FALSE;
        if ($this->userAuth instanceof UserAuthenticationInterface) {
          $authenticated = $this->userAuth->authenticateAccount(
            $account, $password
          );
        }
        else {
          $authenticated = $this->userAuth->authenticate($username, $password);
        }

        if ($authenticated) {
          $this->flood->clear('mcp_auth.failed_login_user', $identifier);

          return $account;
        }
        else {
          $this->flood->register(
            'mcp_auth.failed_login_user', $flood_config->get('user_window'),
            $identifier
          );
        }
      }
    }

    return NULL;
  }

  /**
   * Authenticates the request using a token.
   */
  private function authenticateToken(
    Request $request,
    string $decoded,
    $flood_config,
  ) {
    $auth_settings = $this->mcpSettings->getAuthSettings();
    if (empty($auth_settings['enable_token_auth'])) {
      return NULL;
    }
    $expectedToken = $auth_settings['token_key_value'] ?? '';
    $token_user_id = $auth_settings['token_user'] ?? '';

    // Validate token user configuration.
    if (empty($token_user_id)) {
      throw new McpAuthException(
        "Token authentication is enabled but no user is configured."
      );
    }

    $identifier = $flood_config->get('uid_only')
      ? $token_user_id
      : $token_user_id . '-' . $request->getClientIP();
    if ($this->flood->isAllowed(
      'mcp_auth.failed_login_token', $flood_config->get('user_limit'),
      $flood_config->get('user_window'), $identifier
    )
    ) {
      if ($decoded === $expectedToken) {
        $this->flood->clear('mcp_auth.failed_login_token', $identifier);

        $user = $this->entityTypeManager
          ->getStorage('user')
          ->load($token_user_id);

        if (!$user || !$user->isActive()) {
          throw new McpAuthException(
            "Configured token user does not exist or is not active."
          );
        }

        return $user;
      }
      else {
        $this->flood->register(
          'mcp_auth.failed_login_token', $flood_config->get('user_window'),
          $identifier
        );
      }
    }

    return NULL;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc