config_preview_deploy-1.0.0-alpha3/src/Controller/OAuthController.php

src/Controller/OAuthController.php
<?php

declare(strict_types=1);

namespace Drupal\config_preview_deploy\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Url;
use Drupal\key\KeyRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use GuzzleHttp\ClientInterface;

/**
 * Handles OAuth 2.0 authorization flow for config deployment.
 */
class OAuthController extends ControllerBase {

  /**
   * The HTTP client.
   */
  protected ClientInterface $httpClient;

  /**
   * The private tempstore factory.
   */
  protected PrivateTempStoreFactory $privateTempStoreFactory;

  /**
   * The key repository service.
   */
  protected KeyRepositoryInterface $keyRepository;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    $instance = parent::create($container);
    $instance->httpClient = $container->get('http_client');
    $instance->privateTempStoreFactory = $container->get('tempstore.private');
    $instance->keyRepository = $container->get('key.repository');
    return $instance;
  }

  /**
   * Handles OAuth callback from production environment.
   *
   * This method receives the authorization code from production OAuth server
   * and exchanges it for an access token.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object containing OAuth callback parameters.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   Redirect to deployment form with token or error.
   */
  public function callback(Request $request): RedirectResponse {
    $code = $request->query->get('code');
    $state = $request->query->get('state');
    $error = $request->query->get('error');

    // Check for OAuth errors.
    if ($error) {
      $error_description = $request->query->get('error_description', 'Unknown OAuth error');
      $this->messenger()->addError($this->t('OAuth authorization failed: @error - @description', [
        '@error' => $error,
        '@description' => $error_description,
      ]));

      return new RedirectResponse(Url::fromRoute('config_preview_deploy.dashboard')->toString());
    }

    // Validate required parameters.
    if (!$code || !$state) {
      $this->messenger()->addError($this->t('Invalid OAuth callback - missing authorization code or state parameter.'));
      return new RedirectResponse(Url::fromRoute('config_preview_deploy.dashboard')->toString());
    }

    // Validate state parameter to prevent CSRF attacks.
    $tempstore = $this->privateTempStoreFactory->get('config_preview_deploy');
    $expected_state = $tempstore->get('oauth_state');

    if (!$expected_state || !hash_equals($expected_state, $state)) {
      $this->messenger()->addError($this->t('Invalid OAuth state parameter - possible CSRF attack.'));
      return new RedirectResponse(Url::fromRoute('config_preview_deploy.dashboard')->toString());
    }

    try {
      // Exchange authorization code for access token.
      $token_data = $this->exchangeCodeForToken($code);

      if ($token_data) {
        // Store access token in session for deployment.
        $tempstore = $this->privateTempStoreFactory->get('config_preview_deploy');
        $tempstore->set('access_token', $token_data['access_token']);
        $tempstore->set('token_type', $token_data['token_type'] ?? 'Bearer');
        $tempstore->set('expires_in', $token_data['expires_in'] ?? 3600);
        $tempstore->set('token_received_at', time());

        // Check target and redirect accordingly.
        $target = $tempstore->get('oauth_target');
        if ($target === 'rebase') {
          $tempstore->delete('oauth_target');
          return new RedirectResponse(Url::fromRoute('config_preview_deploy.rebase_form')->toString());
        }

        // Default redirect to deploy form.
        return new RedirectResponse(Url::fromRoute('config_preview_deploy.deploy_form')->toString());
      }
      else {
        $this->messenger()->addError($this->t('Failed to exchange authorization code for access token.'));
      }
    }
    catch (\Exception $e) {
      $this->getLogger('config_preview_deploy')->error('OAuth token exchange failed: @message', [
        '@message' => $e->getMessage(),
      ]);

      $this->messenger()->addError($this->t('OAuth authorization failed due to technical error.'));
    }

    return new RedirectResponse(Url::fromRoute('config_preview_deploy.dashboard')->toString());
  }

  /**
   * Initiates OAuth authorization flow with production environment.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   Redirect to production OAuth authorize endpoint.
   */
  public function authorize(Request $request): RedirectResponse {
    $config = $this->config('config_preview_deploy.settings');
    $production_url = $config->get('production_url');

    if (!$production_url) {
      $this->messenger()->addError($this->t('Production URL not configured. Please configure it in module settings.'));
      return new RedirectResponse(Url::fromRoute('config_preview_deploy.dashboard')->toString());
    }

    // Load OAuth consumer configuration.
    $consumers = $this->entityTypeManager()
      ->getStorage('consumer')
      ->loadByProperties(['client_id' => 'config_preview_deploy']);

    if (empty($consumers)) {
      $this->messenger()->addError($this->t('OAuth consumer not found. Please reinstall the module.'));
      return new RedirectResponse(Url::fromRoute('config_preview_deploy.dashboard')->toString());
    }

    $consumer = reset($consumers);

    // Generate and store state parameter for CSRF protection.
    $state = bin2hex(random_bytes(16));
    $tempstore = $this->privateTempStoreFactory->get('config_preview_deploy');
    $tempstore->set('oauth_state', $state);

    // Store target if provided.
    $target = $request->query->get('target');
    if ($target) {
      $tempstore->set('oauth_target', $target);
    }

    // Build authorization URL.
    $callback_url = Url::fromRoute('config_preview_deploy.oauth_callback', [], ['absolute' => TRUE])->toString();

    $authorize_params = [
      'response_type' => 'code',
      'client_id' => $consumer->getClientId(),
      'redirect_uri' => $callback_url,
      'scope' => 'config_preview_deploy',
      'state' => $state,
    ];

    $authorize_url = rtrim($production_url, '/') . '/oauth/authorize?' . http_build_query($authorize_params);

    return new TrustedRedirectResponse($authorize_url);
  }

  /**
   * Exchanges authorization code for access token.
   *
   * @param string $code
   *   The authorization code from OAuth callback.
   *
   * @return array|null
   *   Token data array or NULL on failure.
   */
  protected function exchangeCodeForToken(string $code): ?array {
    $config = $this->config('config_preview_deploy.settings');
    $production_url = $config->get('production_url');

    // Load OAuth consumer.
    $consumers = $this->entityTypeManager()
      ->getStorage('consumer')
      ->loadByProperties(['client_id' => 'config_preview_deploy']);

    if (empty($consumers)) {
      throw new \RuntimeException($this->t('OAuth consumer not found')->render());
    }

    $consumer = reset($consumers);
    $callback_url = Url::fromRoute('config_preview_deploy.oauth_callback', [], ['absolute' => TRUE])->toString();

    // Prepare token exchange request.
    $token_params = [
      'grant_type' => 'authorization_code',
      'code' => $code,
      'redirect_uri' => $callback_url,
      'client_id' => $consumer->getClientId(),
      // Get client secret from Key module.
      'client_secret' => $this->keyRepository->getKey('config_deploy_secret')?->getKeyValue(),
    ];

    $token_url = rtrim($production_url, '/') . '/oauth/token';

    try {
      $response = $this->httpClient->post($token_url, [
        'form_params' => $token_params,
        'headers' => [
          'Accept' => 'application/json',
          'Content-Type' => 'application/x-www-form-urlencoded',
        ],
        'timeout' => 30,
      ]);

      $token_data = json_decode($response->getBody()->getContents(), TRUE);

      if (isset($token_data['access_token'])) {
        return $token_data;
      }
      else {
        $this->getLogger('config_preview_deploy')->error('Token exchange response missing access_token');
        return NULL;
      }
    }
    catch (\Exception $e) {
      $this->getLogger('config_preview_deploy')->error('Token exchange HTTP error: @message', [
        '@message' => $e->getMessage(),
      ]);
      throw $e;
    }
  }

}

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

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