oidc-1.0.0-alpha2/src/EventSubscriber/RequestSubscriber.php

src/EventSubscriber/RequestSubscriber.php
<?php

namespace Drupal\oidc\EventSubscriber;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\Session\SessionManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Utility\Error;
use Drupal\oidc\OpenidConnectSessionInterface;
use Drupal\oidc\Routing\ImmutableTrustedRedirectResponse;
use Drupal\Core\Url;
use Drupal\oidc\Token;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;

/**
 * Event subscriber for requests.
 */
class RequestSubscriber extends KernelSubscriberBase {

  use LoggerChannelTrait;
  use MessengerTrait;
  use StringTranslationTrait;

  /**
   * The request matcher service.
   *
   * @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface
   */
  protected $requestMatcher;

  /**
   * The session manager service.
   *
   * @var \Drupal\Core\Session\SessionManagerInterface
   */
  protected $sessionManager;

  /**
   * The OpenID Connect session service.
   *
   * @var \Drupal\oidc\OpenidConnectSessionInterface
   */
  protected $session;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * The time service.
   *
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected $time;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * Route match cache.
   *
   * @var array[]
   */
  protected $matches = [];

  /**
   * Class constructor.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The configuration factory service.
   * @param \Symfony\Component\Routing\Matcher\RequestMatcherInterface $request_matcher
   *   The request matcher service.
   * @param \Drupal\Core\Session\SessionManagerInterface $session_manager
   *   The session manager service.
   * @param \Drupal\oidc\OpenidConnectSessionInterface $session
   *   The OpenID Connect session service.
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   The current user.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   */
  public function __construct(ConfigFactoryInterface $config_factory, RequestMatcherInterface $request_matcher, SessionManagerInterface $session_manager, OpenidConnectSessionInterface $session, AccountProxyInterface $current_user, TimeInterface $time, ModuleHandlerInterface $module_handler) {
    parent::__construct($config_factory);

    $this->requestMatcher = $request_matcher;
    $this->sessionManager = $session_manager;
    $this->session = $session;
    $this->currentUser = $current_user;
    $this->time = $time;
    $this->moduleHandler = $module_handler;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = ['onRequest', 100];

    return $events;
  }

  /**
   * Act on the request.
   *
   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
   *   The request event.
   */
  public function onRequest(RequestEvent $event) {
    try {
      $request = $event->getRequest();

      // Ignore unkown and OpenID Connect routes.
      $match = $this->getRouteMatch($request);
      if (!$match || str_starts_with($match['_route'], 'oidc.openid_connect.')) {
        return;
      }

      // Clear the state.
      $this->session->clearState();

      // Route based redirection.
      if ($this->setRedirectForRoute($event)) {
        return;
      }

      // Ensure the access token is still valid.
      if ($this->session->isAuthenticated()) {
        $this->ensureValidAccessToken($event);
      }
    }
    catch (\Exception $ex) {
      // A kernel request subscriber shouldn't throw any exceptions.
      $this->getLogger('oidc')->error(
        '%type: @message in %function (line %line of %file).',
        Error::decodeException($ex)
      );
    }
  }

  /**
   * Get the route match.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request to match.
   *
   * @return array|null
   *   The route match or NULL if unknown.
   */
  protected function getRouteMatch(Request $request) {
    $path = $request->getPathInfo();

    if (str_starts_with($path, '/system/files/') && !$request->query->has('file')) {
      // Private files paths are split by the inbound path processor and the
      // relative file path is moved to the 'file' query string parameter. This
      // is because the route system does not allow an arbitrary amount of
      // parameters. Therefore, we don't match and redirect private files.
      // @see \Drupal\system\PathProcessor\PathProcessorFiles::processInbound()
      return NULL;
    }

    if (array_key_exists($path, $this->matches)) {
      return $this->matches[$path];
    }

    try {
      $match = $this->requestMatcher->matchRequest($request);
    }
    catch (\Exception $ex) {
      $match = NULL;
    }

    $this->matches[$path] = $match;

    return $match;
  }

  /**
   * Set a redirect response if the route requires it.
   *
   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
   *   The request event.
   *
   * @return bool
   *   TRUE if a redirect was set, FALSE otherwise.
   */
  protected function setRedirectForRoute(RequestEvent $event) {
    $request = $event->getRequest();

    if (!$match = $this->getRouteMatch($request)) {
      return FALSE;
    }

    $url = NULL;
    $destination = $request->query->get('destination');

    switch ($match['_route']) {
      case 'user.login':
        if ($this->currentUser->isAnonymous() && !$request->query->has('local')) {
          $url = $this->getLoginUrl($destination);
        }
        break;

      case 'user.logout':
        if ($this->session->isAuthenticated()) {
          $options = [];
          if ($destination !== NULL && $destination !== '') {
            $options['query']['destination'] = $destination;
          }

          $url = Url::fromRoute('oidc.openid_connect.logout', [], $options);
        }
        break;
    }

    if ($url) {
      // Set the redirect response.
      return $this->setRedirectIfNotCurrentRoute($event, $url);
    }

    return FALSE;
  }

  /**
   * Ensure the access token is still valid.
   *
   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
   *   The request event.
   */
  protected function ensureValidAccessToken(RequestEvent $event) {
    if (!$tokens = $this->session->getJsonWebTokens()) {
      return;
    }

    // Check if the access token has expired.
    if (!$this->isTokenExpired($tokens->getAccessToken())) {
      return;
    }

    // Logout if there's no valid refresh token.
    if ($this->isTokenExpired($tokens->getRefreshToken())) {
      $this->logout($event);
      return;
    }

    // Get the realm plugin.
    $plugin = $this->session->getRealmPlugin();

    try {
      // Update the tokens.
      $tokens = $plugin->getJsonWebTokensforRefresh($tokens->getRefreshToken());
      $this->session->setJsonWebTokens($tokens);
    }
    catch (\Exception $ex) {
      $this->getLogger('oidc')->error('Error on refresh via @realm realm: %error', [
        '@realm' => $plugin->getPluginId(),
        '%error' => $ex->getMessage(),
      ]);

      $this->logout($event);
      return;
    }
  }

  /**
   * Check if a token has expired.
   *
   * @param \Drupal\oidc\Token|null $token
   *   The token.
   *
   * @return bool
   *   TRUE if it's expired, FALSE otherwise.
   */
  protected function isTokenExpired(?Token $token = NULL) {
    return !$token || $token->getExpires() <= $this->time->getRequestTime();
  }

  /**
   * End the user session.
   *
   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
   *   The request event.
   */
  protected function logout(RequestEvent $event) {
    // Log out of Drupal. This code is copied from user_logout() because that function also
    // destroys the PHP session, which results in the expired session message getting lost.
    $this->getLogger('user')->notice('Session closed for %name.', ['%name' => $this->currentUser->getAccountName()]);
    $this->moduleHandler->invokeAll('user_logout', [$this->currentUser]);
    $this->currentUser->setAccount(new AnonymousUserSession());

    if (!$this->settings->get('show_session_expired_message')) {
      // No message to be set, destroy the session.
      $this->sessionManager->destroy();
    }
    else {
      // Clear the session and set a message.
      $this->sessionManager->clear();
      $this->messenger()->addWarning($this->t('Your session has ended because the remote session expired.'));
    }

    // Redirect to the front page.
    $this->setRedirectIfNotCurrentRoute($event, Url::fromRoute('<front>'));
  }

  /**
   * Redirect to a URL if it isn't the current route.
   *
   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
   *   The request event.
   * @param \Drupal\Core\Url $url
   *   The URL to redirect to.
   *
   * @return bool
   *   TRUE if a redirect was set, FALSE otherwise.
   */
  protected function setRedirectIfNotCurrentRoute(RequestEvent $event, Url $url) {
    $match = $this->getRouteMatch($event->getRequest());

    // Set the redirect response.
    if (!$match || !$url->isRouted() || $url->getRouteName() !== $match['_route']) {
      $event->setResponse(new ImmutableTrustedRedirectResponse($url));

      return TRUE;
    }

    return FALSE;
  }

}

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

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