openid_connect_rest-8.x-1.0-rc2/src/Controller/ApiController.php

src/Controller/ApiController.php
<?php

namespace Drupal\openid_connect_rest\Controller;

use Exception;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\DependencyInjection\ContainerInterface;

use Drupal\Core\Url;
use Drupal\Core\Database\Connection;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountProxy;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;

use Drupal\Component\Uuid\Php;

use GuzzleHttp\ClientInterface;

use Drupal\openid_connect\Claims;
use Drupal\openid_connect\Authmap;
use Drupal\openid_connect\Controller\RedirectController;

use Drupal\openid_connect_rest\Entity\StateToken;
use Drupal\openid_connect_rest\Entity\AuthorizationMapping;
use Drupal\openid_connect_rest\Plugin\OpenIDConnectRESTClientManager;

/**
 * Class ApiController.
 *
 * @package Drupal\openid_connect_rest\Controller
 */
class ApiController extends ControllerBase {

  /**
   * OpenIDConnectRESTClientManager definition.
   *
   * @var \Drupal\openid_connect_rest\Plugin\OpenIDConnectRESTClientManager
   */
  protected $pluginManager;

  /**
   * The request stack used to access request globals.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * The HTTP client to fetch the feed data with.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected $httpClient;

  /**
   * Drupal\Core\Session\AccountProxy definition.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * Provides language support.
   *
   * @var \Drupal\Core\Language\LanguageManager
   */
  protected $languageManager;

  /**
   * The Base Database API.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * The Entity API.
   *
   * @var \Drupal\Core\Entity\EntityTypeManager
   */
  protected $entityTypeManager;

  /**
   * The logger factory.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerFactory;

  /**
   * The uuid utility.
   *
   * @var \Drupal\Component\Uuid\Php
   */
  protected $uuid;

  /**
   * The OpenID Connect claims.
   *
   * @var \Drupal\openid_connect\Claims
   */
  protected $claims;

  /**
   * The OpenID Authorization mapping service.
   *
   * @var \Drupal\openid_connect\Authmap
   */
  protected $authmap;

  /**
   * The OpenID Connect redirect controller.
   *
   * @var \Drupal\openid_connect\Controller\RedirectController
   */
  protected $redirectController;

  /**
   * {@inheritdoc}
   *
   * The constructor.
   *
   * @param \Drupal\openid_connect_rest\Plugin\OpenIDConnectRESTClientManager $plugin_manager
   *   The plugin manager.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack used to access request globals.
   * @param \GuzzleHttp\ClientInterface $http_client
   *   The HTTP client to fetch the feed data with.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   Drupal\Core\Session\AccountProxy definition.
   * @param \Drupal\Core\Language\LanguageManager $language_manager
   *   Provides language support.
   * @param \Drupal\Core\Database\Connection $database
   *   The Base Database API.
   * @param \Drupal\Core\Entity\EntityTypeManager $entity_type_manager
   *   The Entity API.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger factory.
   * @param \Drupal\Component\Uuid\Php $uuid
   *   The uuid utility.
   * @param \Drupal\openid_connect\Claims $claims
   *   The OpenID Connect claims.
   * @param \Drupal\openid_connect\Authmap $authmap
   *   The OpenID Authorization mapping service.
   * @param \Drupal\openid_connect\Controller\RedirectController $redirect_controller
   *   The OpenID Connect redirect controller.
   */
  public function __construct(
      OpenIDConnectRESTClientManager $plugin_manager,
      RequestStack $request_stack,
      ClientInterface $http_client,
      AccountInterface $current_user,
      LanguageManager $language_manager,
      Connection $database,
      EntityTypeManager $entity_type_manager,
      LoggerChannelFactoryInterface $logger_factory,
      Php $uuid,
      Claims $claims,
      Authmap $authmap,
      RedirectController $redirect_controller
  ) {

    $this->pluginManager = $plugin_manager;

    $this->requestStack = $request_stack;
    $this->httpClient = $http_client;
    $this->currentUser = $current_user;
    $this->languageManager = $language_manager;
    $this->database = $database;
    $this->entityTypeManager = $entity_type_manager;

    $this->loggerFactory = $logger_factory;
    $this->uuid = $uuid;

    $this->claims = $claims;
    $this->authmap = $authmap;
    $this->redirectController = $redirect_controller;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('plugin.manager.openid_connect_rest_client.processor'),
      $container->get('request_stack'),
      $container->get('http_client'),
      $container->get('current_user'),
      $container->get('language_manager'),
      $container->get('database'),
      $container->get('entity_type.manager'),
      $container->get('logger.factory'),
      $container->get('uuid'),
      $container->get('openid_connect.claims'),
      $container->get('openid_connect.authmap'),
      $container->get('openid_connect_rest.openid_connect.redirect_controller')
    );
  }

  /**
   * Access callback for ApiController::getToken().
   *
   * @return bool
   *   Whether the authorization has a match in the authorization mappings.
   */
  public function canAccessGetToken() {
    $currentRequest = $this->requestStack->getCurrentRequest();

    $state_token = $currentRequest->request->get('state');
    $authorization_code = $currentRequest->request->get('code');
    $authorization_mapping = $this->findAuthorizationMapping($authorization_code, $state_token);
    if (!empty($authorization_mapping) && is_object($authorization_mapping)) {
      if ($this->userSubIsValid($authorization_mapping)) {
        return AccessResult::allowed();
      }
      $authorization_mapping->delete();
    }

    return AccessResult::forbidden();
  }

  /**
   * Access callback for for ApiController::authenticate().
   *
   * @return bool
   *   Whether the authorization has a match in the authorization mappings.
   */
  public function canAccessAuthenticate() {
    if ($this->redirectController->access()->isAllowed()) {
      return AccessResult::allowed();
    }
    else {
      $currentRequest = $this->requestStack->getCurrentRequest();

      $state_token = $currentRequest->query->get('state');
      $state_token = $this->findStateToken($state_token);
      if (!empty($state_token) && is_object($state_token)) {
        if ($state_token->expires >= ((new DrupalDateTime())->format('U'))) {
          return AccessResult::allowed();
        }
        $state_token->delete();
      }
      return AccessResult::forbidden();
    }
  }

  /**
   * Display success of failure page.
   *
   * @param string $client_name
   *   The provider id.
   *
   * @return array
   *   The renderable array.
   */
  public function authenticate($client_name) {
    $provider_id = $client_name;

    $authorized = FALSE;
    $user = $this->currentUser;
    if (!empty($provider_id)) {
      if ($user->isAuthenticated()) {
        if ($this->isValidProviderId($provider_id)) {
          $user_sub = $this->findUserSub($user);
          if (!empty($user_sub)) {
            $authorized = openid_connect_rest_openid_connect_post_authorize([], $user, [
              'sub' => $user_sub,
            ], $provider_id);
          }
        }
      }
      else {
        $this->redirectController->authenticate($provider_id);
        $authorized = TRUE;
      }
    }

    // State token can be safely deleted from database.
    $currentRequest = $this->requestStack->getCurrentRequest();
    $state_token = $currentRequest->query->get('state');
    $this->deleteStateToken($state_token);

    if ($authorized && $user->isAuthenticated()) {
      $authenticated = TRUE;
      $message = $this->t('You have been successfully authenticated by @provider_id', [
        '@provider_id' => $provider_id,
      ]);
    }
    else {
      $authenticated = FALSE;
      $message = $this->t('We could not authenticate you with @provider_id', [
        '@provider_id' => $provider_id,
      ]);
    }

    return [
      '#message' => $message,
      '#authenticated' => $authenticated,
      '#theme' => 'openid_connect_rest_authenticate_page',
    ];
  }

  /**
   * Oauth token endpoint.
   *
   * @param string $provider_id
   *   The provider id.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   The json response with the authentication token.
   */
  public function getToken($provider_id) {
    $json_response = [
      'error' => 'OpenID Connect REST API error',
      'message' => $this->t('Sorry, something went wrong.'),
    ];

    $configuration = $this->config('openid_connect.settings.' . $provider_id);
    if (!empty($configuration)) {
      $settings = $configuration->get('settings');
      if (!empty($settings)) {
        $provider = $this->pluginManager->createInstance(
          $provider_id,
          $settings
        );
      }
    }

    if (!empty($provider)) {
      $currentRequest = $this->requestStack->getCurrentRequest();

      $state_token = $currentRequest->request->get('state');
      $authorization_code = $currentRequest->request->get('code');
      $authorization_mapping = $this->findAuthorizationMapping($authorization_code, $state_token);

      if (!empty($authorization_mapping) && is_object($authorization_mapping)) {
        if ($this->userSubIsValid($authorization_mapping)) {
          $sub = $authorization_mapping->user_sub;
          $account = $this->authmap->userLoadBySub($sub, $provider->getPluginId());
          if ($account) {
            $hashed_password = $account->getPassword();

            if (!empty($hashed_password)) {
              // Set a new temporary user password.
              $account->setPassword($sub);
              $account->save();
              $json_response = $this->getBearerFromOauth($account->getUsername(), $sub);
              // Restore the old password.
              $this->setHashedUserPassword($account->id(), $hashed_password);
            }
          }
        }
        $authorization_mapping->delete();
      }
    }

    return new JsonResponse($json_response);
  }

  /**
   * Provider ids endpoint.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   The json response with the available provider ids.
   */
  public function getProviderIds() {
    $definitions = $this->pluginManager->getDefinitions();
    $json_response = [];
    foreach ($definitions as $provider_id => $provider) {
      if (!$this->config('openid_connect.settings.' . $provider_id)
        ->get('enabled')) {
        continue;
      }

      $json_response[$provider_id] = $this->t('Log in with @provider_label', [
        '@provider_label' => $provider['label'],
      ]);
    }
    return new JsonResponse($json_response);
  }

  /**
   * Provider authorization URL endpoint.
   *
   * @param string $provider_id
   *   The provider id.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   The json response with the provider authorization endpoint URL.
   */
  public function getAuthorizationEndpoint($provider_id) {
    $json_response = [];
    if ($this->isValidProviderId($provider_id)) {
      openid_connect_save_destination();
      $configuration = $this->config('openid_connect.settings.' . $provider_id)
        ->get('settings');
      $provider = $this->pluginManager->createInstance(
        $provider_id,
        $configuration
      );

      $scopes = $this->claims->getScopes();
      $_SESSION['openid_connect_op'] = 'login';
      $response = $provider->authorize($scopes);

      $language_none = $this->languageManager
        ->getLanguage(LanguageInterface::LANGCODE_NOT_APPLICABLE);

      $redirect_ctrl_internal_path = (Url::fromRoute('openid_connect.redirect_controller_redirect', [
        'client_name' => $provider_id,
      ], [
        'absolute' => FALSE,
        'language' => $language_none,
      ]))->getInternalPath();

      $rest_api_ctrl_internal_path = (Url::fromRoute('openid_connect_rest.api.authenticate', [
        'client_name' => $provider_id,
      ], [
        'absolute' => FALSE,
        'language' => $language_none,
      ]))->getInternalPath();

      $target_url = $response->getTargetUrl();

      $target_url = str_replace(
        $redirect_ctrl_internal_path,
        $rest_api_ctrl_internal_path,
        $target_url
      );

      $components = $this->getUriComponents($target_url);

      if (!empty($components['parameters']['state'])) {
        if ($this->storeStateToken($components['parameters']['state'])) {
          $json_response['target_url'] = $target_url;
          $json_response['components'] = $components;
        }
        else {
          $json_response = [
            'error' => 'OpenID Connect REST API error',
            'message' => $this->t('Unable to create state token.'),
          ];
        }
      }
      else {
        $json_response = [
          'error' => 'OpenID Connect REST API error',
          'message' => $this->t('Invalid state token.'),
        ];
      }
    }
    else {
      $json_response = [
        'error' => 'OpenID Connect REST API error',
        'message' => $this->t('Invalid provider id.'),
      ];
    }

    return new JsonResponse($json_response);
  }

  /**
   * Gets the uri components from a valid uri.
   *
   * @param string $uri
   *   The uri.
   *
   * @return array
   *   The uri components.
   */
  private function getUriComponents($uri) {
    $components = [
      'base_url' => NULL,
      'parameters' => NULL,
    ];
    if (!empty($uri)) {
      $uri = explode('?', $uri, 2);
      if (count($uri)) {
        if (!empty($uri[0])) {
          $components['base_url'] = $uri[0];
        }

        if (!empty($uri[1])) {
          $components['parameters'] = [];
          parse_str($uri[1], $components['parameters']);
        }
      }
    }
    return $components;
  }

  /**
   * Gets the token from Oauth2.
   *
   * @param string $username
   *   The user username.
   * @param string $password
   *   The user password.
   *
   * @return array
   *   The Oauth2 returned data or an array describing the error.
   */
  private function getBearerFromOauth($username, $password) {
    $currentRequest = $this->requestStack->getCurrentRequest();

    $client_id = $currentRequest->request->get('client_id');
    $client_secret = $currentRequest->request->get('client_secret');
    if (!empty($username) && !empty($password) && !empty($client_id) && !empty($client_secret)) {
      $request_options = [
        'form_params' => [
          'grant_type' => 'password',
          'client_id' => $client_id,
          'client_secret' => $client_secret,
          'username' => $username,
          'password' => $password,
        ],
        'http_errors' => FALSE,
        'allow_redirects' => FALSE,
      ];

      /* @var \GuzzleHttp\ClientInterface $client */
      $client = $this->httpClient;
      try {
        $language_none = $this->languageManager
          ->getLanguage(LanguageInterface::LANGCODE_NOT_APPLICABLE);
        $raw_response = $client->request('POST', (Url::fromRoute('oauth2_token.token', [], [
          'absolute' => TRUE,
          'language' => $language_none,
        ]))->toString(), $request_options);
        $json_response = json_decode((string) $raw_response->getBody(), TRUE);
        return $json_response;
      }
      catch (Exception $e) {
        return [
          'error' => 'OpenID Connect REST API error',
          'message' => $e->getMessage(),
        ];
      }
      return [
        'error' => 'OpenID Connect REST API error',
        'message' => $this->t('Could not get a valid token from OAuth service. Missing some mandatory data.'),
      ];
    }
    return [
      'error' => 'OpenID Connect REST API error',
      'message' => $this->t('Could not get a valid token from OAuth service.'),
    ];
  }

  /**
   * Sets the a user already hashed password.
   *
   * @param string $user_id
   *   The user id.
   * @param string $hashed_password
   *   The user hashed password.
   *
   * @return bool
   *   Whether the update has succeeded or not.
   */
  private function setHashedUserPassword($user_id, $hashed_password) {
    if (!empty($user_id) && !empty($hashed_password)) {
      $updated = $this->database->update('users_field_data')
        ->condition('uid', $user_id)
        ->fields([
          'pass' => $hashed_password,
        ])
        ->execute();
      return $updated;
    }
    return FALSE;
  }

  /**
   * Checks if a provider is valid.
   *
   * @param string $provider_id
   *   The provider id.
   *
   * @return bool
   *   Whether the provider is valid or not.
   */
  private function isValidProviderId($provider_id) {
    $definitions = $this->pluginManager->getDefinitions();
    if (!empty($provider_id) && !empty($definitions[$provider_id])) {
      if ($this->config('openid_connect.settings.' . $provider_id)
        ->get('enabled')) {
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * Checks if a user sub is valid.
   *
   * @param \Drupal\openid_connect_rest\Entity\AuthorizationMapping $authorization_mapping
   *   An authorization mapping.
   *
   * @return bool
   *   Whether the user sub is valid or not.
   */
  private function userSubIsValid(AuthorizationMapping $authorization_mapping) {
    if (!empty($authorization_mapping) && is_object($authorization_mapping)) {
      if (!empty($authorization_mapping->expires) && !empty($authorization_mapping->user_sub)) {
        if ($authorization_mapping->expires >= ((new DrupalDateTime())->format('U'))) {
          return TRUE;
        }
      }
    }
    return FALSE;
  }

  /**
   * Finds a user sub.
   *
   * @param \Drupal\Core\Session\AccountProxy $user
   *   A fully qualified user.
   *
   * @return mixed
   *   A user sub or null.
   */
  private function findUserSub(AccountProxy $user) {
    if ($user->id()) {
      $records = $this->database->select('openid_connect_authmap', 'a')
        ->fields('a', [
          'sub',
        ])
        ->condition('uid', $user->id())
        ->execute();
      if (!empty($records)) {
        $record = $records->fetchAssoc();
        if (!empty($record) && !empty($record['sub'])) {
          return $record['sub'];
        }
      }
    }
    return NULL;
  }

  /**
   * Finds a state token.
   *
   * @param string $state_token
   *   An OpenID Connect state token.
   *
   * @return mixed
   *   A state token or null.
   */
  private function findStateToken($state_token) {
    if (!empty($state_token)) {
      $storage = $this->entityTypeManager->getStorage('state_token');
      $state_token_ids = $storage->getQuery()
        ->condition('state_token', $state_token)
        ->range(0, 1)
        ->execute();
      if (!empty($state_token_ids)) {
        $state_tokens = $storage->loadMultiple($state_token_ids);
        if (!empty($state_tokens) && is_array($state_tokens)) {
          $state_token = current($state_tokens);
          if (!empty($state_token)) {
            return $state_token;
          }
        }
      }
    }
    return NULL;
  }

  /**
   * Insert or updates a state token.
   *
   * @param string $new_state_token
   *   An OpenID Connect state token.
   *
   * @return bool
   *   Whether to state token is stored or not.
   */
  private function storeStateToken($new_state_token) {
    $state_token = $this->findStateToken($new_state_token);
    if ($state_token) {
      $state_token->expires = ((new DrupalDateTime())->format('U')) + 1800;
    }
    else {
      $state_token = StateToken::create([
        'id' => $this->uuid->generate(),
        'state_token' => $new_state_token,
        'expires' => ((new DrupalDateTime())->format('U')) + 1800,
      ]);
    }

    try {
      $state_token->save();
      return TRUE;
    }
    catch (Exception $e) {
      return FALSE;
    }
  }

  /**
   * Finds an authorization mapping.
   *
   * @param string $authorization_code
   *   A provider authorization code.
   * @param string $state_token
   *   An OpenID Connect state token.
   *
   * @return mixed
   *   An authorization mapping or null.
   */
  private function findAuthorizationMapping($authorization_code, $state_token) {
    if (!empty($authorization_code) && !empty($state_token)) {
      $storage = $this->entityTypeManager->getStorage('authorization_mapping');
      $authorization_mapping_ids = $storage->getQuery()
        ->condition('authorization_code', $authorization_code)
        ->condition('state_token', $state_token)
        ->range(0, 1)
        ->execute();
      if (!empty($authorization_mapping_ids)) {
        $authorization_mappings = $storage->loadMultiple($authorization_mapping_ids);
        if (!empty($authorization_mappings) && is_array($authorization_mappings)) {
          $authorization_mapping = current($authorization_mappings);
          if (!empty($authorization_mapping)) {
            return $authorization_mapping;
          }
        }
      }
    }
    return NULL;
  }

  /**
   * Deletes a state token.
   *
   * @param string $state_token
   *   A state token.
   */
  private function deleteStateToken($state_token) {
    if (!empty($state_token)) {
      $state_token = $this->findStateToken($state_token);
      if (!empty($state_token) && is_object($state_token)) {
        $state_token->delete();
      }
    }
  }

}

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

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