You are here

class ServiceController in CAS 2.x

Same name and namespace in other branches
  1. 8 src/Controller/ServiceController.php \Drupal\cas\Controller\ServiceController

Controller used when redirect back from CAS authentication.

Hierarchy

Expanded class hierarchy of ServiceController

1 file declares its use of ServiceController
ServiceControllerTest.php in tests/src/Unit/Controller/ServiceControllerTest.php

File

src/Controller/ServiceController.php, line 32

Namespace

Drupal\cas\Controller
View source
class ServiceController implements ContainerInjectionInterface {
  use StringTranslationTrait;

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

  /**
   * Used to validate CAS service tickets.
   *
   * @var \Drupal\cas\Service\CasValidator
   */
  protected $casValidator;

  /**
   * Used to log a user in after they've been validated.
   *
   * @var \Drupal\cas\Service\CasUserManager
   */
  protected $casUserManager;

  /**
   * Used to log a user out due to a single log out request.
   *
   * @var \Drupal\cas\Service\CasLogout
   */
  protected $casLogout;

  /**
   * Used to retrieve request parameters.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * Used to generate redirect URLs.
   *
   * @var \Drupal\Core\Routing\UrlGeneratorInterface
   */
  protected $urlGenerator;

  /**
   * Stores settings object.
   *
   * @var \Drupal\Core\Config\Config
   */
  protected $settings;

  /**
   * Stores a Messenger object.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * The event dispatcher service.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $eventDispatcher;

  /**
   * The external auth service.
   *
   * @var \Drupal\externalauth\ExternalAuthInterface
   */
  protected $externalAuth;

  /**
   * Constructor.
   *
   * @param \Drupal\cas\Service\CasHelper $cas_helper
   *   The CAS Helper service.
   * @param \Drupal\cas\Service\CasValidator $cas_validator
   *   The CAS Validator service.
   * @param \Drupal\cas\Service\CasUserManager $cas_user_manager
   *   The CAS User Manager service.
   * @param \Drupal\cas\Service\CasLogout $cas_logout
   *   The CAS Logout service.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
   *   The URL generator.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher service.
   * @param \Drupal\externalauth\ExternalAuthInterface $external_auth
   *   The external auth service.
   */
  public function __construct(CasHelper $cas_helper, CasValidator $cas_validator, CasUserManager $cas_user_manager, CasLogout $cas_logout, RequestStack $request_stack, UrlGeneratorInterface $url_generator, ConfigFactoryInterface $config_factory, MessengerInterface $messenger, EventDispatcherInterface $event_dispatcher, ExternalAuthInterface $external_auth) {
    $this->casHelper = $cas_helper;
    $this->casValidator = $cas_validator;
    $this->casUserManager = $cas_user_manager;
    $this->casLogout = $cas_logout;
    $this->requestStack = $request_stack;
    $this->urlGenerator = $url_generator;
    $this->settings = $config_factory
      ->get('cas.settings');
    $this->messenger = $messenger;
    $this->eventDispatcher = $event_dispatcher;
    $this->externalAuth = $external_auth;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('cas.helper'), $container
      ->get('cas.validator'), $container
      ->get('cas.user_manager'), $container
      ->get('cas.logout'), $container
      ->get('request_stack'), $container
      ->get('url_generator'), $container
      ->get('config.factory'), $container
      ->get('messenger'), $container
      ->get('event_dispatcher'), $container
      ->get('externalauth.externalauth'));
  }

  /**
   * Main point of communication between CAS server and the Drupal site.
   *
   * The path that this controller/action handle are always set to the "service"
   * url when authenticating with the CAS server, so CAS server communicates
   * back to the Drupal site using this controller action. That's why there's
   * so much going on in here - it needs to process a few different types of
   * requests.
   */
  public function handle() {
    $request = $this->requestStack
      ->getCurrentRequest();

    // First, check if this is a single-log-out (SLO) request from the server.
    if ($request->request
      ->has('logoutRequest')) {
      try {
        $this->casLogout
          ->handleSlo($request->request
          ->get('logoutRequest'));
      } catch (CasSloException $e) {
        $this->casHelper
          ->log(LogLevel::ERROR, 'Error when handling single-log-out request: %error', [
          '%error' => $e
            ->getMessage(),
        ]);
      }

      // Always return a 200 response. CAS Server doesn’t care either way what
      // happens here, since it is a fire-and-forget approach taken.
      return Response::create('', 200);
    }

    /* If there is no ticket parameter on the request, the browser either:
     * (a) is returning from a gateway request to the CAS server in which
     *     the user was not already authenticated to CAS, so there is no
     *     service ticket to validate and nothing to do.
     * (b) has hit this URL for some other reason (crawler, curiosity, etc)
     *     and there is nothing to do.
     * In either case, we just want to redirect them away from this controller.
     */
    if (!$request->query
      ->has('ticket')) {
      $this->casHelper
        ->log(LogLevel::DEBUG, "No CAS ticket found in request to service controller; backing out.");
      $this->casHelper
        ->handleReturnToParameter($request);
      return RedirectResponse::create($this->urlGenerator
        ->generate('<front>'));
    }

    // There is a ticket present, meaning CAS server has returned the browser
    // to the Drupal site so we can authenticate the user locally using the
    // ticket.
    $ticket = $request->query
      ->get('ticket');

    // Our CAS service will need to reconstruct the original service URL
    // when validating the ticket. We always know what the base URL for
    // the service URL is (it's this page), but there may be some query params
    // attached as well (like a destination param) that we need to pass in
    // as well. So, detach the ticket param, and pass the rest off.
    $service_params = $request->query
      ->all();
    unset($service_params['ticket']);
    try {
      $cas_validation_info = $this->casValidator
        ->validateTicket($ticket, $service_params);
    } catch (CasValidateException $e) {

      // Validation failed, redirect to homepage and set message.
      $this->casHelper
        ->log(LogLevel::ERROR, 'Error when validating ticket: %error', [
        '%error' => $e
          ->getMessage(),
      ]);
      $message_validation_failure = $this->casHelper
        ->getMessage('error_handling.message_validation_failure');
      if (!empty($message_validation_failure)) {
        $this->messenger
          ->addError($message_validation_failure);
      }
      return $this
        ->createRedirectResponse($request, TRUE);
    }
    $this->casHelper
      ->log(LogLevel::DEBUG, 'Starting login process for CAS user %username', [
      '%username' => $cas_validation_info
        ->getUsername(),
    ]);

    // Dispatch an event that allows modules to alter any of the CAS data before
    // it's used to lookup a Drupal user account via the authmap table.
    $this->casHelper
      ->log(LogLevel::DEBUG, 'Dispatching EVENT_PRE_USER_LOAD.');
    $this->eventDispatcher
      ->dispatch(CasHelper::EVENT_PRE_USER_LOAD, new CasPreUserLoadEvent($cas_validation_info));
    if ($cas_validation_info
      ->getUsername() !== $cas_validation_info
      ->getOriginalUsername()) {
      $this->casHelper
        ->log(LogLevel::DEBUG, 'Username was changed from %original to %new from a subscriber.', [
        '%original' => $cas_validation_info
          ->getOriginalUsername(),
        '%new' => $cas_validation_info
          ->getUsername(),
      ]);
    }

    // At this point, the ticket is validated and third-party modules got the
    // chance to alter the username and also perform other 'pre user load'
    // tasks. Before authenticating the user locally, let's allow third-party
    // code to inject user interaction into the flow.
    // @see \Drupal\cas\Event\CasPreUserLoadRedirectEvent
    $cas_pre_user_load_redirect_event = new CasPreUserLoadRedirectEvent($ticket, $cas_validation_info, $service_params);
    $this->casHelper
      ->log(LogLevel::DEBUG, 'Dispatching EVENT_PRE_USER_LOAD_REDIRECT.');
    $this->eventDispatcher
      ->dispatch(CasHelper::EVENT_PRE_USER_LOAD_REDIRECT, $cas_pre_user_load_redirect_event);

    // A subscriber might have set an HTTP redirect response allowing potential
    // user interaction to be injected into the flow.
    $redirect_response = $cas_pre_user_load_redirect_event
      ->getRedirectResponse();
    if ($redirect_response) {
      $this->casHelper
        ->log(LogLevel::DEBUG, 'Redirecting to @url as requested by one of EVENT_PRE_USER_LOAD event subscribers.', [
        '@url' => $redirect_response
          ->getTargetUrl(),
      ]);
      return $redirect_response;
    }

    // Now that the ticket has been validated, we can use the information from
    // validation request to authenticate the user locally on the Drupal site.
    try {
      $this->casUserManager
        ->login($cas_validation_info, $ticket);
      $login_success_message = $this->casHelper
        ->getMessage('login_success_message');
      if (!empty($login_success_message)) {
        $this->messenger
          ->addStatus($login_success_message);
      }
    } catch (CasLoginException $e) {

      // Use an appropriate log level depending on exception type.
      if (empty($e
        ->getCode()) || $e
        ->getCode() === CasLoginException::ATTRIBUTE_PARSING_ERROR) {
        $error_level = LogLevel::ERROR;
      }
      else {
        $error_level = LogLevel::INFO;
      }
      $this->casHelper
        ->log($error_level, $e
        ->getMessage());

      // Display error message to the user, unless this login failure originated
      // from a gateway login. No sense in showing them an error when the login
      // is optional.
      $login_error_message = $this
        ->getLoginErrorMessage($e);
      if ($login_error_message && !$request->query
        ->has('from_gateway')) {
        $this->messenger
          ->addError($login_error_message, 'error');
      }
      return $this
        ->createRedirectResponse($request, TRUE);
    }
    return $this
      ->createRedirectResponse($request);
  }

  /**
   * Create a redirect response that sends users somewhere after login.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request.
   * @param bool $login_failed
   *   Indicates if the login failed or not.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   The redirect response.
   */
  private function createRedirectResponse(Request $request, $login_failed = FALSE) {

    // If login failed, we may have a failure page to send them to. Don't do it
    // if the request was from a gateway auth attempt though, as the login was
    // optional.
    if ($login_failed && $this->settings
      ->get('error_handling.login_failure_page') && !$request->query
      ->has('from_gateway')) {

      // Remove 'destination' parameter, otherwise Drupal's
      // RedirectResponseSubscriber will send users to that location instead of
      // the failure page.
      $request->query
        ->remove('destination');
      return RedirectResponse::create(Url::fromUserInput($this->settings
        ->get('error_handling.login_failure_page'))
        ->toString());
    }
    else {
      $this->casHelper
        ->handleReturnToParameter($request);
      return RedirectResponse::create($this->urlGenerator
        ->generate('<front>'));
    }
  }

  /**
   * Get the error message to display when there is a login exception.
   *
   * @param \Drupal\cas\Exception\CasLoginException $e
   *   The login exception.
   *
   * @return array|\Drupal\Core\StringTranslation\TranslatableMarkup|string
   *   The error message.
   */
  private function getLoginErrorMessage(CasLoginException $e) {
    $code = $e
      ->getCode();
    switch ($code) {
      case CasLoginException::NO_LOCAL_ACCOUNT:
        $msgKey = 'message_no_local_account';
        break;
      case CasLoginException::SUBSCRIBER_DENIED_REG:

        // If a subscriber has denied the registration by setting a custom
        // message, use that message and exit here.
        $message = $e
          ->getSubscriberCancelReason();
        if ($message) {
          return $message;
        }
        $msgKey = 'message_subscriber_denied_reg';
        break;
      case CasLoginException::ACCOUNT_BLOCKED:
        $msgKey = 'message_account_blocked';
        break;
      case CasLoginException::SUBSCRIBER_DENIED_LOGIN:

        // If a subscriber has denied the login by setting a custom message, use
        // that message and exit here.
        $message = $e
          ->getSubscriberCancelReason();
        if ($message) {
          return $message;
        }
        $msgKey = 'message_subscriber_denied_login';
        break;
      case CasLoginException::ATTRIBUTE_PARSING_ERROR:

        // Re-use the normal validation error message.
        $msgKey = 'message_validation_failure';
        break;
      case CasLoginException::USERNAME_ALREADY_EXISTS:
        $msgKey = 'message_username_already_exists';
        break;
    }
    if (!empty($msgKey)) {
      $message = $this->casHelper
        ->getMessage('error_handling.' . $msgKey);
      if ($message) {
        return $message;
      }
    }
    return $this
      ->t('There was a problem logging in. Please contact a site administrator.');
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ServiceController::$casHelper protected property CAS Helper.
ServiceController::$casLogout protected property Used to log a user out due to a single log out request.
ServiceController::$casUserManager protected property Used to log a user in after they've been validated.
ServiceController::$casValidator protected property Used to validate CAS service tickets.
ServiceController::$eventDispatcher protected property The event dispatcher service.
ServiceController::$externalAuth protected property The external auth service.
ServiceController::$messenger protected property Stores a Messenger object.
ServiceController::$requestStack protected property Used to retrieve request parameters.
ServiceController::$settings protected property Stores settings object.
ServiceController::$urlGenerator protected property Used to generate redirect URLs.
ServiceController::create public static function Instantiates a new instance of this class. Overrides ContainerInjectionInterface::create
ServiceController::createRedirectResponse private function Create a redirect response that sends users somewhere after login.
ServiceController::getLoginErrorMessage private function Get the error message to display when there is a login exception.
ServiceController::handle public function Main point of communication between CAS server and the Drupal site.
ServiceController::__construct public function Constructor.
StringTranslationTrait::$stringTranslation protected property The string translation service. 4
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.