You are here

class CasSubscriber in CAS 8

Provides a CasSubscriber.

Hierarchy

Expanded class hierarchy of CasSubscriber

1 string reference to 'CasSubscriber'
cas.services.yml in ./cas.services.yml
cas.services.yml
1 service uses CasSubscriber
cas.subscriber in ./cas.services.yml
Drupal\cas\Subscriber\CasSubscriber

File

src/Subscriber/CasSubscriber.php, line 24

Namespace

Drupal\cas\Subscriber
View source
class CasSubscriber extends HttpExceptionSubscriberBase {

  /**
   * The request.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * Route matcher object.
   *
   * @var \Drupal\Core\Routing\RouteMatchInterface
   */
  protected $routeMatcher;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

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

  /**
   * Condition manager.
   *
   * @var \Drupal\Core\Condition\ConditionManager
   */
  protected $conditionManager;

  /**
   * CAS helper.
   *
   * @var \Drupal\cas\Service\CasHelper
   */
  protected $casHelper;

  /**
   * CasRedirector.
   *
   * @var \Drupal\cas\Service\CasRedirector
   */
  protected $casRedirector;

  /**
   * Frequency to check for gateway login.
   *
   * @var array
   */
  protected $gatewayCheckFrequency;

  /**
   * Paths to check for gateway login.
   *
   * @var array
   */
  protected $gatewayPaths = [];

  /**
   * Is forced login configuration setting enabled.
   *
   * @var bool
   */
  protected $forcedLoginEnabled = FALSE;

  /**
   * Paths to check for forced login.
   *
   * @var array
   */
  protected $forcedLoginPaths = [];

  /**
   * Constructs a new CasSubscriber.
   *
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request.
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_matcher
   *   The route matcher.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   * @param \Drupal\Core\Condition\ConditionManager $condition_manager
   *   The condition manager.
   * @param \Drupal\cas\Service\CasHelper $cas_helper
   *   The CAS Helper service.
   * @param \Drupal\cas\Service\CasRedirector $cas_redirector
   *   The CAS Redirector Service.
   */
  public function __construct(RequestStack $request_stack, RouteMatchInterface $route_matcher, ConfigFactoryInterface $config_factory, AccountInterface $current_user, ConditionManager $condition_manager, CasHelper $cas_helper, CasRedirector $cas_redirector) {
    $this->requestStack = $request_stack;
    $this->routeMatcher = $route_matcher;
    $this->configFactory = $config_factory;
    $this->currentUser = $current_user;
    $this->conditionManager = $condition_manager;
    $this->casHelper = $cas_helper;
    $this->casRedirector = $cas_redirector;
    $settings = $this->configFactory
      ->get('cas.settings');
    $this->gatewayCheckFrequency = $settings
      ->get('gateway.check_frequency');
    $this->gatewayPaths = $settings
      ->get('gateway.paths');
    $this->forcedLoginPaths = $settings
      ->get('forced_login.paths');
    $this->forcedLoginEnabled = $settings
      ->get('forced_login.enabled');
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {

    // Priority is just before the Dynamic Page Cache subscriber, but after
    // important services like route matcher and maintenance mode subscribers.
    $events[KernelEvents::REQUEST][] = [
      'handle',
      29,
    ];
    $events[KernelEvents::EXCEPTION][] = [
      'onException',
      0,
    ];
    return $events;
  }

  /**
   * The entry point for our subscriber.
   *
   * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
   *   The response event from the kernel.
   */
  public function handle(GetResponseEvent $event) {

    // Don't do anything if this is a sub request and not a master request.
    if ($event
      ->getRequestType() != HttpKernelInterface::MASTER_REQUEST) {
      return;
    }

    // Some routes we don't want to run on.
    if ($this
      ->isIgnoreableRoute()) {
      return;
    }

    // The service controller may have indicated that this current request
    // should not be automatically sent to CAS for authentication checking.
    // This is to prevent infinite redirect loops.
    $current_request = $this->requestStack
      ->getCurrentRequest();
    $session = $current_request
      ->getSession();
    if ($session && $session
      ->has('cas_temp_disable_auto_auth')) {
      $session
        ->remove('cas_temp_disable_auto_auth');
      $this->casHelper
        ->log(LogLevel::DEBUG, "Temp disable flag set, skipping CAS subscriber.");
      return;
    }

    // Add the current path to the service URL as the 'destination' param,
    // so that when the ServiceController eventually processess the login,
    // it knows to return the user back here.
    $current_uri = $current_request
      ->getUri();
    $current_scheme_and_host = $current_request
      ->getSchemeAndHttpHost();
    $current_path = str_replace($current_scheme_and_host, '', $current_uri);
    $redirect_data = new CasRedirectData([
      'destination' => $current_path,
    ]);

    // Nothing to do if the user is already logged in.
    if ($this->currentUser
      ->isAuthenticated()) {
      $redirect_data
        ->preventRedirection();
    }
    else {

      // Default assumption is that we don't want to redirect unless page
      // critera matches.
      $redirect_data
        ->preventRedirection();

      // Check to see if we should initiate a gateway auth check.
      if ($this
        ->handleGateway()) {
        $redirect_data
          ->setParameter('gateway', 'true');
        $this->casHelper
          ->log(LogLevel::DEBUG, 'Initializing gateway auth from CasSubscriber.');
        $redirect_data
          ->forceRedirection();
      }

      // Check to see if we should require a forced login.
      if ($this
        ->handleForcedPath()) {
        $this->casHelper
          ->log(LogLevel::DEBUG, 'Initializing forced login auth from CasSubscriber.');
        $redirect_data
          ->setParameter('gateway', NULL);
        $redirect_data
          ->setIsCacheable(TRUE);
        $redirect_data
          ->forceRedirection();
      }
    }

    // If we're still going to redirect, lets do it.
    $response = $this->casRedirector
      ->buildRedirectResponse($redirect_data);
    if ($response) {
      $event
        ->setResponse($response);

      // If there's a 'destination' parameter set on the current request,
      // remove it, otherwise Drupal's RedirectResponseSubscriber will send
      // users to that location instead of to our CAS server.
      $current_request->query
        ->remove('destination');
    }
  }

  /**
   * Check if the current path is a forced login path.
   *
   * @return bool
   *   TRUE if current path is a forced login path, FALSE otherwise.
   */
  private function handleForcedPath() {
    if (!$this->forcedLoginEnabled) {
      return FALSE;
    }

    // Check if user provided specific paths to force/not force a login.
    $condition = $this->conditionManager
      ->createInstance('request_path');
    $condition
      ->setConfiguration($this->forcedLoginPaths);
    if ($this->conditionManager
      ->execute($condition)) {
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Check if the current path is a gateway path.
   *
   * @return bool
   *   TRUE if current path is a gateway path, FALSE otherwise.
   */
  private function handleGateway() {

    // Don't do anything if this is a request from cron, drush, crawler, etc.
    if ($this
      ->isCrawlerRequest()) {
      return FALSE;
    }

    // Only implement gateway feature for GET requests, to prevent users from
    // being redirected to CAS server for things like form submissions.
    if (!$this->requestStack
      ->getCurrentRequest()
      ->isMethod('GET')) {
      return FALSE;
    }
    if ($this->gatewayCheckFrequency === CasHelper::CHECK_NEVER) {
      return FALSE;
    }

    // User can indicate specific paths to enable (or disable) gateway mode.
    $condition = $this->conditionManager
      ->createInstance('request_path');
    $condition
      ->setConfiguration($this->gatewayPaths);
    if (!$this->conditionManager
      ->execute($condition)) {
      return FALSE;
    }

    // If set to only implement gateway once per session, we use a session
    // variable to store the fact that we've already done the gateway check
    // so we don't keep doing it.
    if ($this->gatewayCheckFrequency === CasHelper::CHECK_ONCE) {

      // If the session var is already set, we know to back out.
      if ($this->requestStack
        ->getCurrentRequest()
        ->getSession()
        ->has('cas_gateway_checked')) {
        $this->casHelper
          ->log(LogLevel::DEBUG, 'CAS gateway auth has already been performed for this session.');
        return FALSE;
      }
      $this->requestStack
        ->getCurrentRequest()
        ->getSession()
        ->set('cas_gateway_checked', TRUE);
    }
    return TRUE;
  }

  /**
   * Check is the current request is from a known list of web crawlers.
   *
   * We don't want to perform any CAS redirects in this case, because crawlers
   * need to be able to index the pages.
   *
   * @return bool
   *   True if the request is coming from a crawler, false otherwise.
   */
  private function isCrawlerRequest() {
    $current_request = $this->requestStack
      ->getCurrentRequest();
    if ($current_request->server
      ->get('HTTP_USER_AGENT')) {
      $crawlers = [
        'Google',
        'msnbot',
        'Rambler',
        'Yahoo',
        'AbachoBOT',
        'accoona',
        'AcoiRobot',
        'ASPSeek',
        'CrocCrawler',
        'Dumbot',
        'FAST-WebCrawler',
        'GeonaBot',
        'Gigabot',
        'Lycos',
        'MSRBOT',
        'Scooter',
        'AltaVista',
        'IDBot',
        'eStyle',
        'Scrubby',
        'gsa-crawler',
      ];

      // Return on the first find.
      foreach ($crawlers as $c) {
        if (stripos($current_request->server
          ->get('HTTP_USER_AGENT'), $c) !== FALSE) {
          $this->casHelper
            ->log(LogLevel::DEBUG, 'CasSubscriber ignoring request from suspected crawler "%crawler"', [
            '%crawler' => $c,
          ]);
          return TRUE;
        }
      }
    }
    return FALSE;
  }

  /**
   * Checks current request route against a list of routes we want to ignore.
   *
   * @return bool
   *   TRUE if we should ignore this request, FALSE otherwise.
   */
  private function isIgnoreableRoute() {
    $routes_to_ignore = [
      'cas.service',
      'cas.proxyCallback',
      'cas.login',
      'cas.legacy_login',
      'cas.logout',
      'system.cron',
    ];
    $current_route = $this->routeMatcher
      ->getRouteName();
    if (in_array($current_route, $routes_to_ignore)) {
      return TRUE;
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  protected function getHandledFormats() {
    return [
      'html',
    ];
  }

  /**
   * Handle 403 errors.
   *
   * Other request subscribers with a higher priority may intercept the request
   * and return a 403 before our request subscriber can handle it. In those
   * instances we handle the forced login redirect if applicable here instead,
   * using an exception subscriber.
   *
   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
   *   The event to process.
   */
  public function on403(GetResponseForExceptionEvent $event) {
    if ($this->currentUser
      ->isAnonymous()) {
      $return_to = $this->requestStack
        ->getCurrentRequest()
        ->getUri();
      $redirect_data = new CasRedirectData([
        'returnto' => $return_to,
      ]);
      if ($this
        ->handleForcedPath()) {
        $this->casHelper
          ->log(LogLevel::DEBUG, 'Initializing forced login auth from CasSubscriber.');
        $redirect_data
          ->forceRedirection();
        $redirect_data
          ->setIsCacheable(TRUE);
      }
      else {
        $redirect_data
          ->preventRedirection();
      }

      // If we're still going to redirect, lets do it.
      $response = $this->casRedirector
        ->buildRedirectResponse($redirect_data);
      if ($response) {
        $event
          ->setResponse($response);
      }
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
CasSubscriber::$casHelper protected property CAS helper.
CasSubscriber::$casRedirector protected property CasRedirector.
CasSubscriber::$conditionManager protected property Condition manager.
CasSubscriber::$configFactory protected property The config factory.
CasSubscriber::$currentUser protected property The current user service.
CasSubscriber::$forcedLoginEnabled protected property Is forced login configuration setting enabled.
CasSubscriber::$forcedLoginPaths protected property Paths to check for forced login.
CasSubscriber::$gatewayCheckFrequency protected property Frequency to check for gateway login.
CasSubscriber::$gatewayPaths protected property Paths to check for gateway login.
CasSubscriber::$requestStack protected property The request.
CasSubscriber::$routeMatcher protected property Route matcher object.
CasSubscriber::getHandledFormats protected function Specifies the request formats this subscriber will respond to. Overrides HttpExceptionSubscriberBase::getHandledFormats
CasSubscriber::getSubscribedEvents public static function Registers the methods in this class that should be listeners. Overrides HttpExceptionSubscriberBase::getSubscribedEvents
CasSubscriber::handle public function The entry point for our subscriber.
CasSubscriber::handleForcedPath private function Check if the current path is a forced login path.
CasSubscriber::handleGateway private function Check if the current path is a gateway path.
CasSubscriber::isCrawlerRequest private function Check is the current request is from a known list of web crawlers.
CasSubscriber::isIgnoreableRoute private function Checks current request route against a list of routes we want to ignore.
CasSubscriber::on403 public function Handle 403 errors.
CasSubscriber::__construct public function Constructs a new CasSubscriber.
HttpExceptionSubscriberBase::getPriority protected static function Specifies the priority of all listeners in this class. 4
HttpExceptionSubscriberBase::onException public function Handles errors for this subscriber.