You are here

class SamlService in SAML Authentication 8.3

Same name and namespace in other branches
  1. 8 src/SamlService.php \Drupal\samlauth\SamlService
  2. 8.2 src/SamlService.php \Drupal\samlauth\SamlService
  3. 4.x src/SamlService.php \Drupal\samlauth\SamlService

Governs communication between the SAML toolkit and the IdP / login behavior.

There's no formal interface here, only a promise to not change things in breaking ways in the 3.x releases. The division in responsibilities between this class and SamlController (which calls most of its public methods) is partly arbitrary. It's roughly "Controller contains code dealing with redirects; SamlService contains the other logic". Code will likely be moved around to new classes in 4.x.

Hierarchy

Expanded class hierarchy of SamlService

1 file declares its use of SamlService
SamlController.php in src/Controller/SamlController.php
1 string reference to 'SamlService'
samlauth.services.yml in ./samlauth.services.yml
samlauth.services.yml
1 service uses SamlService
samlauth.saml in ./samlauth.services.yml
Drupal\samlauth\SamlService

File

src/SamlService.php, line 40

Namespace

Drupal\samlauth
View source
class SamlService {
  use StringTranslationTrait;

  /**
   * Auth objects (usually 0 or 1) representing the current request state.
   *
   * @var \OneLogin\Saml2\Auth[]
   */
  protected $samlAuth;

  /**
   * The ExternalAuth service.
   *
   * @var \Drupal\externalauth\ExternalAuth
   */
  protected $externalAuth;

  /**
   * The Authmap service.
   *
   * @var \Drupal\externalauth\Authmap
   */
  protected $authmap;

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

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

  /**
   * A logger instance.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

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

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

  /**
   * Private store for SAML session data.
   *
   * @var \Drupal\Core\TempStore\PrivateTempStore
   */
  protected $privateTempStore;

  /**
   * The flood service.
   *
   * @var \Drupal\Core\Flood\FloodInterface
   */
  protected $flood;

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

  /**
   * The messenger service.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * The Key repository service.
   *
   * This is set when the key module is installed. (Not when the key_asymmetric
   * module is installed. The latter is necessary for entering public/private
   * keys but reading them will work fine without it, it seems.)
   *
   * @var \Drupal\key\KeyRepositoryInterface
   */
  protected $keyRepository;

  /**
   * Constructs a new SamlService.
   *
   * @param \Drupal\externalauth\ExternalAuth $external_auth
   *   The ExternalAuth service.
   * @param \Drupal\externalauth\Authmap $authmap
   *   The Authmap service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The EntityTypeManager service.
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
   *   A temp data store factory object.
   * @param \Drupal\Core\Flood\FloodInterface $flood
   *   The flood service.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user service.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger service.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
   *   The string translation service.
   */
  public function __construct(ExternalAuth $external_auth, Authmap $authmap, ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, LoggerInterface $logger, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack, PrivateTempStoreFactory $temp_store_factory, FloodInterface $flood, AccountInterface $current_user, MessengerInterface $messenger, TranslationInterface $translation) {
    $this->externalAuth = $external_auth;
    $this->authmap = $authmap;
    $this->configFactory = $config_factory;
    $this->entityTypeManager = $entity_type_manager;
    $this->logger = $logger;
    $this->eventDispatcher = $event_dispatcher;
    $this->requestStack = $request_stack;
    $this->privateTempStore = $temp_store_factory
      ->get('samlauth');
    $this->flood = $flood;
    $this->currentUser = $current_user;
    $this->messenger = $messenger;
    $this
      ->setStringTranslation($translation);
    $config = $this->configFactory
      ->get('samlauth.authentication');

    // setProxyVars lets the SAML PHP Toolkit use 'X-Forwarded-*' HTTP headers
    // for identifying the SP URL, but we should pass the Drupal/Symfony base
    // URL to into the toolkit instead. That uses headers/trusted values in the
    // same way as the rest of Drupal (as configured in settings.php).
    // @todo remove this in v4.x
    if ($config
      ->get('use_proxy_headers') && !$config
      ->get('use_base_url')) {

      // Use 'X-Forwarded-*' HTTP headers for identifying the SP URL.
      SamlUtils::setProxyVars(TRUE);
    }
  }

  /**
   * Set the Key repository service.
   *
   * This has a separate setter (unlike all other dependent objects mentioned
   * in the constructor) because it's an optional dependency.
   *
   * @param \Drupal\key\KeyRepositoryInterface $key_repository
   *   the Key repository service.
   */
  public function setKeyRepository(KeyRepositoryInterface $key_repository) {
    $this->keyRepository = $key_repository;
  }

  /**
   * Show metadata about the local sp. Use this to configure your saml2 IdP.
   *
   * @param int|null $validity
   *   (Optional) 'validUntil' property of the metadata (which is a date, not
   *   an interval) will be this many seconds into the future. If left empty,
   *   the SAML PHP Toolkit will assign a value.
   * @param int|null $cache_duration
   *   (Optional) number of seconds used for the 'cacheDuration' property of
   *   the metadata. If left empty, the SAML PHP Toolkit will assign a value.
   *
   * @return mixed
   *   XML string representing metadata.
   *
   * @throws \OneLogin\Saml2\Error
   *   If the metatdad is invalid.
   */
  public function getMetadata($validity = NULL, $cache_duration = NULL) {

    // It's actually strange how we need to instantiate an Auth object when
    // we only need the Settings object. We may refactor that when refactoring
    // getSamlAuth().
    $settings = $this
      ->getSamlAuth('metadata')
      ->getSettings();
    $metadata = $settings
      ->getSPMetadata(FALSE, $validity, $cache_duration);
    $errors = $settings
      ->validateMetadata($metadata);
    if (empty($errors)) {
      return $metadata;
    }
    else {
      throw new SamlError('Invalid SP metadata: ' . implode(', ', $errors), SamlError::METADATA_SP_INVALID);
    }
  }

  /**
   * Initiates a SAML2 authentication flow and redirects to the IdP.
   *
   * @param string $return_to
   *   (optional) The path to return the user to after successful processing by
   *   the IdP.
   * @param array $parameters
   *   (optional) Extra query parameters to add to the returned redirect URL.
   *
   * @return string
   *   The URL of the single sign-on service to redirect to, including query
   *   parameters.
   */
  public function login($return_to = NULL, array $parameters = []) {
    $config = $this->configFactory
      ->get('samlauth.authentication');
    $url = $this
      ->getSamlAuth('login')
      ->login($return_to, $parameters, FALSE, FALSE, TRUE, $config
      ->get('request_set_name_id_policy') ?? TRUE);
    if ($config
      ->get('debug_log_saml_out')) {
      $this->logger
        ->debug('Sending SAML authentication request: <pre>@message</pre>', [
        '@message' => $this
          ->getSamlAuth('login')
          ->getLastRequestXML(),
      ]);
    }
    return $url;
  }

  /**
   * Processes a SAML response (Assertion Consumer Service).
   *
   * First checks whether the SAML request is OK, then takes action on the
   * Drupal user (logs in / maps existing / create new) depending on attributes
   * sent in the request and our module configuration.
   *
   * @return bool
   *   TRUE if the response was correctly processed; FALSE if an error was
   *   encountered while processing but there's a currently logged-in user and
   *   we decided not to throw an exception for this case.
   *
   * @throws \Exception
   */
  public function acs() {
    $config = $this->configFactory
      ->get('samlauth.authentication');
    if ($config
      ->get('debug_log_in')) {
      if (isset($_POST['SAMLResponse'])) {
        $response = base64_decode($_POST['SAMLResponse']);
        if ($response) {
          $this->logger
            ->debug("ACS received 'SAMLResponse' in POST request (base64 decoded): <pre>@message</pre>", [
            '@message' => $response,
          ]);
        }
        else {
          $this->logger
            ->warning("ACS received 'SAMLResponse' in POST request which could not be base64 decoded: <pre>@message</pre>", [
            '@message' => $_POST['SAMLResponse'],
          ]);
        }
      }
      else {

        // Not sure if we should be more detailed...
        $this->logger
          ->warning("HTTP request to ACS is not a POST request, or contains no 'SAMLResponse' parameter.");
      }
    }

    // Perform flood control. This is not to guard against failed login
    // attempts per se; that is the IdP's job. It's just protection against
    // a flood of bogus (DDoS-like) requests because this route performs
    // computationally expensive operations. So: just IP based flood control,
    // using the limit / window values that Core uses for regular login.
    $flood_config = $this->configFactory
      ->get('user.flood');
    if (!$this->flood
      ->isAllowed('samlauth.failed_login_ip', $flood_config
      ->get('ip_limit'), $flood_config
      ->get('ip_window'))) {
      throw new TooManyRequestsHttpException(NULL, 'Access is blocked because of IP based flood prevention.');
    }

    // Process the ACS response message and check if we can derive a linked
    // account, but don't process errors yet. (The following code is a kludge
    // because we may need the linked account / may ignore errors later.)
    try {
      $this
        ->processLoginResponse();
    } catch (\Exception $acs_exception) {
    }
    if (!isset($acs_exception)) {
      $unique_id = $this
        ->getAttributeByConfig('unique_id_attribute');
      if ($unique_id) {
        $account = $this->externalAuth
          ->load($unique_id, 'samlauth') ?: NULL;
      }
    }
    $logout_different_user = $config
      ->get('logout_different_user');
    if ($this->currentUser
      ->isAuthenticated()) {

      // Either redirect or log out so that we can log a different user in.
      // 'Redirecting' is done by the caller - so we can just return from here.
      if (isset($account) && $account
        ->id() === $this->currentUser
        ->id()) {

        // Noting that we were already logged in probably isn't useful. (Core's
        // user/reset link isn't a good case to compare: it always logs the
        // user out and presents the "Reset password" form with a login button.
        // 'drush uli' links, at least on D7, display an info message "please
        // reset your password" because they land on the user edit form.)
        return !isset($acs_exception);
      }
      if (!$logout_different_user) {

        // Message similar to when a user/reset link is followed.
        $this->messenger
          ->addWarning($this
          ->t('Another user (%other_user) is already logged into the site on this computer, but you tried to log in as user %login_user through an external authentication provider. Please <a href=":logout">log out</a> and try again.', [
          '%other_user' => $this->currentUser
            ->getAccountName(),
          '%login_user' => $account ? $account
            ->getAccountName() : '?',
          // Point to /user/logout rather than /saml/logout because we don't
          // want to make people log out from all their logged-in sites.
          ':logout' => Url::fromRoute('user.logout')
            ->toString(),
        ]));
        return !isset($acs_exception);
      }

      // If the SAML response indicates (/ if the processing generated) an
      // error, we don't want to log the current user out but we want to
      // clearly indicate that someone else is still logged in.
      if (isset($acs_exception)) {
        $this->messenger
          ->addWarning($this
          ->t('Another user (%other_user) is already logged into the site on this computer. You tried to log in through an external authentication provider, which failed, so the user is still logged in.', [
          '%other_user' => $this->currentUser
            ->getAccountName(),
        ]));
      }
      else {
        $this
          ->drupalLogoutHelper();
        $this->messenger
          ->addStatus($this
          ->t('Another user (%other_user) was already logged into the site on this computer, and has now been logged out.', [
          '%other_user' => $this->currentUser
            ->getAccountName(),
        ]));
      }
    }
    if (isset($acs_exception)) {
      $this->flood
        ->register('samlauth.failed_login_ip', $flood_config
        ->get('ip_window'));
      throw $acs_exception;
    }
    if (!$unique_id) {
      throw new \RuntimeException('Configured unique ID is not present in SAML response.');
    }
    $this
      ->doLogin($unique_id, $account);

    // Remember SAML session values that may be necessary for logout.
    $auth = $this
      ->getSamlAuth('acs');
    $values = [
      'session_index' => $auth
        ->getSessionIndex(),
      'session_expiration' => $auth
        ->getSessionExpiration(),
      'name_id' => $auth
        ->getNameId(),
      'name_id_format' => $auth
        ->getNameIdFormat(),
    ];
    foreach ($values as $key => $value) {
      if (isset($value)) {
        $this->privateTempStore
          ->set($key, $value);
      }
      else {
        $this->privateTempStore
          ->delete($key);
      }
    }
    return TRUE;
  }

  /**
   * Processes a SAML authentication response; throws an exception if invalid.
   *
   * The mechanics of checking whether there are any errors are not so
   * straightforward, so this helper function hopes to abstract that away.
   *
   * @todo should we also check a Response against the ID of the request we
   *   sent earlier? Seems to be not absolutely required on top of the validity
   *   / signature checks which the library already does - but every extra
   *   check is good. Maybe make it optional.
   */
  protected function processLoginResponse() {
    $config = $this->configFactory
      ->get('samlauth.authentication');
    $auth = $this
      ->getSamlAuth('acs');

    // This call can throw various kinds of exceptions if the 'SAMLResponse'
    // request parameter is not present or cannot be decoded into a valid SAML
    // (XML) message, and can also set error conditions instead - if the XML
    // contains data that is not considered valid. We should likely treat all
    // error conditions the same.
    $auth
      ->processResponse();
    if ($config
      ->get('debug_log_saml_in')) {
      $this->logger
        ->debug('ACS received SAML response: <pre>@message</pre>', [
        '@message' => $auth
          ->getLastResponseXML(),
      ]);
    }
    $errors = $auth
      ->getErrors();
    if ($errors) {

      // We have one or multiple error types / short descriptions, and one
      // 'reason' for the last error.
      throw new \RuntimeException('Error(s) encountered during processing of authentication response. Type(s): ' . implode(', ', array_unique($errors)) . '; reason given for last error: ' . $auth
        ->getLastErrorReason());
    }
    if (!$auth
      ->isAuthenticated()) {

      // Looking at the current code, isAuthenticated() just means "response
      // is valid" because it is mutually exclusive with $errors and exceptions
      // being thrown. So we should never get here. We're just checking it in
      // case the library code changes - in which case we should reevaluate.
      throw new \RuntimeException('SAML authentication response was apparently not fully validated even when no error was provided.');
    }
  }

  /**
   * Logs a user in, creating / linking an account; synchronizes attributes.
   *
   * Split off from acs() to... have at least some kind of split.
   *
   * @param string $unique_id
   *   The unique ID (attribute value) contained in the SAML response.
   * @param \Drupal\Core\Session\AccountInterface|null $account
   *   The existing user account derived from the unique ID, if any.
   */
  protected function doLogin($unique_id, AccountInterface $account = NULL) {
    $config = $this->configFactory
      ->get('samlauth.authentication');
    $first_saml_login = FALSE;
    if (!$account) {
      $this->logger
        ->debug('No matching local users found for unique SAML ID @saml_id.', [
        '@saml_id' => $unique_id,
      ]);

      // Try to link an existing user: first through a custom event handler,
      // then by name, then by email.
      if ($config
        ->get('map_users')) {
        $event = new SamlauthUserLinkEvent($this
          ->getAttributes());
        $this->eventDispatcher
          ->dispatch(SamlauthEvents::USER_LINK, $event);
        $account = $event
          ->getLinkedAccount();
        if ($account) {
          $this->logger
            ->info('Existing user @name (@uid) was newly matched to SAML login attributes; linking user and logging in.', [
            '@name' => $account
              ->getAccountName(),
            '@uid' => $account
              ->id(),
          ]);
        }
      }

      // Linking by name / email: we also select accounts if they are blocked
      // (and throw an exception later on) because 1) we don't want the
      // selection to be dependent on the current account's state; 2) name and
      // email are unique and would otherwise lead to another error while
      // trying to create a new account with duplicate values.
      if (!$account) {
        $name = $this
          ->getAttributeByConfig('user_name_attribute');
        if ($name && ($account_search = $this->entityTypeManager
          ->getStorage('user')
          ->loadByProperties([
          'name' => $name,
        ]))) {
          $account = current($account_search);
          if ($config
            ->get('map_users_name')) {
            $this->logger
              ->info('SAML login for name @name (as provided in a SAML attribute) matches existing Drupal account @uid; linking account and logging in.', [
              '@name' => $name,
              '@uid' => $account
                ->id(),
            ]);
          }
          else {

            // We're not configured to link the account by name, but we still
            // looked it up by name so we can give a better error message than
            // the one caused by trying to save a new account with a duplicate
            // name, later.
            $this->logger
              ->warning('Denying login: SAML login for unique ID @saml_id matches existing Drupal account name @name and we are not configured to automatically link accounts.', [
              '@saml_id' => $unique_id,
              '@name' => $account
                ->getAccountName(),
            ]);
            throw new UserVisibleException('A local user account with your login name already exists, and we are disallowed from linking it.');
          }
        }
      }
      if (!$account) {
        $mail = $this
          ->getAttributeByConfig('user_mail_attribute');
        if ($mail && ($account_search = $this->entityTypeManager
          ->getStorage('user')
          ->loadByProperties([
          'mail' => $mail,
        ]))) {
          $account = current($account_search);
          if ($config
            ->get('map_users_mail')) {
            $this->logger
              ->info('SAML login for email @mail (as provided in a SAML attribute) matches existing Drupal account @uid; linking account and logging in.', [
              '@mail' => $mail,
              '@uid' => $account
                ->id(),
            ]);
          }
          else {

            // Treat duplicate email same as duplicate name above.
            $this->logger
              ->warning('Denying login: SAML login for unique ID @saml_id matches existing Drupal account email @mail and we are not configured to automatically link the account.', [
              '@saml_id' => $unique_id,
              '@mail' => $account
                ->getEmail(),
            ]);
            throw new UserVisibleException('A local user account with your login email address name already exists, and we are disallowed from linking it.');
          }
        }
      }
      if ($account) {
        $this
          ->linkExistingAccount($unique_id, $account);
        $first_saml_login = TRUE;
      }
    }

    // If we haven't found an account to link, create one from the SAML
    // attributes.
    if (!$account) {
      if ($config
        ->get('create_users')) {

        // The register() call will save the account. We want to:
        // - add values from the SAML response into the user account;
        // - not save the account twice (because if the second save fails we do
        //   not want to end up with a user account in an undetermined state);
        // - reuse code (i.e. call synchronizeUserAttributes() with its current
        //   signature, which is also done when an existing user logs in).
        // Because of the third point, we are not passing the necessary SAML
        // attributes into register()'s $account_data parameter, but we want to
        // hook into the save operation of the user account object that is
        // created by register(). It seems we can only do this by implementing
        // hook_user_presave() - which calls our synchronizeUserAttributes().
        $account_data = [
          'name' => $this
            ->getAttributeByConfig('user_name_attribute'),
        ];
        $account = $this->externalAuth
          ->register($unique_id, 'samlauth', $account_data);
        $this->externalAuth
          ->userLoginFinalize($account, $unique_id, 'samlauth');
      }
      else {
        throw new UserVisibleException('No existing user account matches the SAML ID provided. This authentication service is not configured to create new accounts.');
      }
    }
    elseif ($account
      ->isBlocked()) {
      throw new UserVisibleException('Requested account is blocked.');
    }
    else {

      // Synchronize the user account with SAML attributes if needed.
      $this
        ->synchronizeUserAttributes($account, FALSE, $first_saml_login);
      $this->externalAuth
        ->userLoginFinalize($account, $unique_id, 'samlauth');
    }
  }

  /**
   * Link a pre-existing Drupal user to a given authname.
   *
   * @param string $unique_id
   *   The unique ID (attribute value) contained in the SAML response.
   * @param \Drupal\Core\Session\AccountInterface|null $account
   *   The existing user account derived from the unique ID, if any.
   *
   * @throws \Drupal\samlauth\UserVisibleException
   *   If linking fails or is denied.
   */
  protected function linkExistingAccount($unique_id, UserInterface $account) {
    $allowed_roles = $this->configFactory
      ->get('samlauth.authentication')
      ->get('map_users_roles');
    $disallowed_roles = array_diff($account
      ->getRoles(), $allowed_roles, [
      AccountInterface::AUTHENTICATED_ROLE,
    ]);
    if ($disallowed_roles) {
      $this->logger
        ->warning('Denying login: SAML login for unique ID @saml_id matches existing Drupal account @uid which we are not allowed to link because it has roles @roles.', [
        '@saml_id' => $unique_id,
        '@uid' => $account
          ->id(),
        '@roles' => implode(', ', $disallowed_roles),
      ]);
      throw new UserVisibleException('A local user account matching your login already exists, and we are disallowed from linking it.');
    }
    $this->externalAuth
      ->linkExistingAccount($unique_id, 'samlauth', $account);

    // linkExistingAccount() does not tell us whether the link was actually
    // successful; it silently continues if the account was already linked
    // to a different unique ID. This would mean a user who has the power
    // to change their user name / email on the IdP side, potentially has
    // the power to log into different accounts (as long as they only log
    // into accounts that already are linked to a different IdP user).
    $linked_id = $this->authmap
      ->get($account
      ->id(), 'samlauth');
    if ($linked_id != $unique_id) {
      $this->logger
        ->warning('Denying login: existing Drupal account @uid matches SAML login for unique ID @saml_id, but the account is already linked to SAML login ID @linked_id. If a new account should be created despite the earlier match, temporarily turn off matching. If this login should be linked to user @uid, remove the earlier link.', [
        '@uid' => $account
          ->id(),
        '@saml_id' => $unique_id,
        '@linked_id' => $linked_id,
      ]);
      throw new UserVisibleException('Your login data match an earlier login by a different SAML user.');
    }
  }

  /**
   * Initiates a SAML2 logout flow and redirects to the IdP.
   *
   * @param string $return_to
   *   (optional) The path to return the user to after successful processing by
   *   the IdP.
   * @param array $parameters
   *   (optional) Extra query parameters to add to the returned redirect URL.
   *
   * @return string
   *   The URL of the single logout service to redirect to, including query
   *   parameters.
   */
  public function logout($return_to = NULL, array $parameters = []) {

    // Log the Drupal user out at the start of the process if they were still
    // logged in. Official SAML documentation usually specifies (as far as it
    // does) that we should log the user out after getting redirected from the
    // IdP instead, at /saml/sls. However
    // - Between calling logout() and all those redirects there is a lot that
    //   could go wrong which would then influence users' ability to log out of
    //   Drupal.
    // - There's no real downside to doing it now, either for the user or for
    //   our code (which already explicitly supports handling users who were
    //   previously logged out of Drupal).
    // - Site administrators may also want this endpoint to work for logging
    //   out non-SAML users. (Otherwise how are they going to display
    //   different login links for different users?) PLEASE NOTE however, that
    //   this is not the primary purpose of this method; it is to enable both
    //   logged-in and already-logged-out Drupal users to start a SAML logout
    //   process - i.e. to be redirected to the IdP. So a side effect is that
    //   non-SAML users are also redirected to the IdP unnecessarily. It may be
    //   possible to prevent this - but that will need to be tested carefully.
    $saml_session_data = $this
      ->drupalLogoutHelper();

    // Start the SAML logout process. If the user was already logged out before
    // this method was called, we won't have any SAML session data so won't be
    // able to tell the IdP which session should be logging out. Even so, the
    // SAML Toolkit is able to create a generic LogoutRequest, and for at least
    // some IdPs that's enough to log the user out from the IdP if applicable
    // (because they have their own browser/cookie based session handling) and
    // return a SAMLResponse indicating success. (Maybe there's some way to
    // modify the Drupal logout process to keep the SAML session data available
    // but we won't explore that until there's a practical situation where
    // that's clearly needed.)
    // @todo should we check session expiration time before sending a logout
    //   request to the IdP? (What would an IdP do if it received an old
    //   session index? Is it better to not redirect, and throw an error on
    //   our side?)
    // @todo include nameId(SP)NameQualifier?
    $url = $this
      ->getSamlAuth('logout')
      ->logout($return_to, $parameters, $saml_session_data['name_id'] ?? NULL, $saml_session_data['session_index'] ?? NULL, TRUE, $saml_session_data['name_id_format'] ?? NULL);
    if ($this->configFactory
      ->get('samlauth.authentication')
      ->get('debug_log_saml_out')) {
      $this->logger
        ->debug('Sending SAML logout request: <pre>@message</pre>', [
        '@message' => $this
          ->getSamlAuth('logout')
          ->getLastRequestXML(),
      ]);
    }
    return $url;
  }

  /**
   * Does processing for the Single Logout Service.
   *
   * @return null|string
   *   Usually returns nothing. May return a URL to redirect to.
   */
  public function sls() {
    $config = $this->configFactory
      ->get('samlauth.authentication');

    // We might at some point check if this code can be abstracted a bit...
    if ($config
      ->get('debug_log_in')) {
      if (isset($_GET['SAMLResponse'])) {
        $response = base64_decode($_GET['SAMLResponse']);
        if ($response) {
          $this->logger
            ->debug("SLS received 'SAMLResponse' in GET request (base64 decoded): <pre>@message</pre>", [
            '@message' => $response,
          ]);
        }
        else {
          $this->logger
            ->warning("SLS received 'SAMLResponse' in GET request which could not be base64 decoded: <pre>@message</pre>", [
            '@message' => $_POST['SAMLResponse'],
          ]);
        }
      }
      elseif (isset($_GET['SAMLRequest'])) {
        $response = base64_decode($_GET['SAMLRequest']);
        if ($response) {
          $this->logger
            ->debug("SLS received 'SAMLRequest' in GET request (base64 decoded): <pre>@message</pre>", [
            '@message' => $response,
          ]);
        }
        else {
          $this->logger
            ->warning("SLS received 'SAMLRequest' in GET request which could not be base64 decoded: <pre>@message</pre>", [
            '@message' => $_POST['SAMLRequest'],
          ]);
        }
      }
      else {

        // Not sure if we should be more detailed...
        $this->logger
          ->warning("HTTP request to SLS is not a GET request, or contains no 'SAMLResponse'/'SAMLRequest' parameters.");
      }
    }

    // Perform flood control; see acs().
    $flood_config = $this->configFactory
      ->get('user.flood');
    if (!$this->flood
      ->isAllowed('samlauth.failed_logout_ip', $flood_config
      ->get('ip_limit'), $flood_config
      ->get('ip_window'))) {
      throw new TooManyRequestsHttpException(NULL, 'Access is blocked because of IP based flood prevention.');
    }
    try {

      // This line means we're extracting logic previously encapsulated inside
      // the Auth class. That's slightly unfortunate but doesn't compare to all
      // other considerations still needing to be made re. refactoring logic.
      $purpose = isset($_GET['SAMLResponse']) ? 'sls-response' : 'sls-request';

      // Unlike the 'logout()' route, we only log the user out if we have a
      // valid request/response, so first have the SAML Toolkit check things.
      // Don't have it do any session actions, because nothing is needed
      // besides our own logout actions (if any). This call can either set an
      // error condition or throw a \OneLogin_Saml2_Error, depending on whether
      // we are processing a POST request; don't catch anything.
      // @todo should we check a LogoutResponse against the ID of the
      //   LogoutRequest we sent earlier? Seems to be not absolutely required on
      //   top of the validity / signature checks which the library already does
      //   - but every extra check is good. Maybe make it optional.
      $url = $this
        ->getSamlAuth($purpose)
        ->processSLO(TRUE, NULL, (bool) $config
        ->get('security_logout_reuse_sigs'), NULL, TRUE);
    } catch (\Exception $e) {
      $this->flood
        ->register('samlauth.failed_logout_ip', $flood_config
        ->get('ip_window'));
      throw $e;
    }
    if ($config
      ->get('debug_log_saml_in')) {

      // There should be no way we can get here if neither GET parameter is set;
      // if nothing gets logged, that's a bug.
      if (isset($_GET['SAMLResponse'])) {
        $this->logger
          ->debug('SLS received SAML response: <pre>@message</pre>', [
          '@message' => $this
            ->getSamlAuth($purpose)
            ->getLastResponseXML(),
        ]);
      }
      elseif (isset($_GET['SAMLRequest'])) {
        $this->logger
          ->debug('SLS received SAML request: <pre>@message</pre>', [
          '@message' => $this
            ->getSamlAuth($purpose)
            ->getLastRequestXML(),
        ]);
      }
    }

    // Now look if there were any errors and also throw.
    $errors = $this
      ->getSamlAuth($purpose)
      ->getErrors();
    if (!empty($errors)) {

      // We have one or multiple error types / short descriptions, and one
      // 'reason' for the last error.
      throw new \RuntimeException('Error(s) encountered during processing of SLS response. Type(s): ' . implode(', ', array_unique($errors)) . '; reason given for last error: ' . $this
        ->getSamlAuth($purpose)
        ->getLastErrorReason());
    }

    // Remove SAML session data, log the user out of Drupal, and return a
    // redirect URL if we got any. Usually,
    // - a LogoutRequest means we need to log out and redirect back to the IdP,
    //   for which the SAML Toolkit returned a URL.
    // - after a LogoutResponse we don't need to log out because we already did
    //   that at the start of the process, in logout() - but there's nothing
    //   against checking. We did not get an URL returned and our caller can
    //   decide what to do next.
    $this
      ->drupalLogoutHelper();
    return $url;
  }

  /**
   * Synchronizes user data with attributes in the SAML request.
   *
   * @param \Drupal\user\UserInterface $account
   *   The Drupal user to synchronize attributes into.
   * @param bool $skip_save
   *   (optional) If TRUE, skip saving the user account.
   * @param bool $first_saml_login
   *   (optional) Indicator of whether the account is newly registered/linked.
   */
  public function synchronizeUserAttributes(UserInterface $account, $skip_save = FALSE, $first_saml_login = FALSE) {

    // Dispatch a user_sync event.
    $event = new SamlauthUserSyncEvent($account, $this
      ->getAttributes(), $first_saml_login);
    $this->eventDispatcher
      ->dispatch(SamlauthEvents::USER_SYNC, $event);
    if (!$skip_save && $event
      ->isAccountChanged()) {
      $account
        ->save();
    }
  }

  /**
   * Returns all attributes in a SAML response.
   *
   * This method will return valid data after a response is processed (i.e.
   * after samlAuth->processResponse() is called).
   *
   * @return array
   *   An array with all returned SAML attributes..
   */
  public function getAttributes() {
    $attributes = $this
      ->getSamlAuth('acs')
      ->getAttributes();
    $friendly_attributes = $this
      ->getSamlAuth('acs')
      ->getAttributesWithFriendlyName();
    return $attributes + $friendly_attributes;
  }

  /**
   * Returns value from a SAML attribute whose name is configured in our module.
   *
   * This method will return valid data after a response is processed (i.e.
   * after samlAuth->processResponse() is called).
   *
   * @param string $config_key
   *   A key in the module's configuration, containing the name of a SAML
   *   attribute.
   *
   * @return mixed|null
   *   The SAML attribute value; NULL if the attribute value, or configuration
   *   key, was not found.
   */
  public function getAttributeByConfig($config_key) {
    $attribute_name = $this->configFactory
      ->get('samlauth.authentication')
      ->get($config_key);
    if ($attribute_name) {
      $attribute = $this
        ->getSamlAuth('acs')
        ->getAttribute($attribute_name);
      if (!empty($attribute[0])) {
        return $attribute[0];
      }
      $friendly_attribute = $this
        ->getSamlAuth('acs')
        ->getAttributeWithFriendlyName($attribute_name);
      if (!empty($friendly_attribute[0])) {
        return $friendly_attribute[0];
      }
    }
  }

  /**
   * Returns an initialized Auth class from the SAML Toolkit.
   *
   * @param string $purpose
   *   (Optional) purpose for the config: 'metadata' / 'login' / 'acs' /
   *   'logout' / 'sls-request' / 'sls-response'. Empty string means 'any', but
   *   likely shouldn't be used anywhere. (The way many callers hardcode this
   *   argument may seem strange, until you realize that _these callers_ only
   *   have one possible purpose too, in practice. This is almost sure to be
   *   refactored away in a future version.)
   */
  protected function getSamlAuth($purpose = '') {
    if (!isset($this->samlAuth[$purpose])) {
      $base_url = '';
      $config = $this->configFactory
        ->get('samlauth.authentication');
      if ($config
        ->get('use_base_url')) {
        $request = $this->requestStack
          ->getCurrentRequest();

        // The 'base url' for the SAML Toolkit is apparently 'all except the
        // last part of the endpoint URLs'. (Whoever wants a better explanation
        // can try to extract it from e.g. Utils::getSelfRoutedURLNoQuery().)
        $base_url = $request
          ->getSchemeAndHttpHost() . $request
          ->getBaseUrl() . '/saml';
      }
      $this->samlAuth[$purpose] = new Auth(static::reformatConfig($config, $base_url, $purpose, $this->keyRepository));
    }
    return $this->samlAuth[$purpose];
  }

  /**
   * Ensures the user is logged out from Drupal; returns SAML session data.
   *
   * @param bool $delete_saml_session_data
   *   (optional) whether to delete the SAML session data. This depends on:
   *   - how bad (privacy sensitive) it is to keep around? Answer: not.
   *   - whether we expect the data to ever be reused. That is: could a SAML
   *     logout attempt be done for the same SAML session multiple times?
   *     Answer: we don't know. Unlikely, because it is not accessible anymore
   *     after logout, so the user would need to log in to Drupal locally again
   *     before anything could be done with it.
   *
   * @return array
   *   Array of data about the 'SAML session' that we stored at login. (The
   *   SAML toolkit itself does not store any data / implement the concept of a
   *   session.)
   */
  protected function drupalLogoutHelper($delete_saml_session_data = TRUE) {
    $data = [];
    if ($this->currentUser
      ->isAuthenticated()) {

      // Get data from our temp store which is not accessible after logout.
      // DEVELOPER NOTE: It depends on our session storage, whether we want to
      // try this for unauthenticated users too. At the moment, we are sure
      // only authenticated users have any SAML session data - and trying to
      // get() a value from our privateTempStore can unnecessarily start a new
      // PHP session for unauthenticated users.
      $keys = [
        'session_index',
        'session_expiration',
        'name_id',
        'name_id_format',
      ];
      foreach ($keys as $key) {
        $data[$key] = $this->privateTempStore
          ->get($key);
        if ($delete_saml_session_data) {
          $this->privateTempStore
            ->delete($key);
        }
      }

      // @todo properly inject this... after #2012976 lands.
      user_logout();
    }
    return $data;
  }

  /**
   * Returns a configuration array as used by the external library.
   *
   * Some of these arguments are just added because the method is static (which
   * will change in v4.x).
   *
   * @param \Drupal\Core\Config\ImmutableConfig $config
   *   The module configuration.
   * @param string $base_url
   *   (Optional) base URL to set.
   * @param string $purpose
   *   (Optional) purpose for the config: 'metadata' / 'login' / 'acs' /
   *   'logout' / 'sls-request' / 'sls-response'.
   * @param \Drupal\key\KeyRepositoryInterface $key_repository
   *   (Optional) the service's Key repository.
   *
   * @return array
   *   The library configuration array.
   */
  protected static function reformatConfig(ImmutableConfig $config, $base_url = '', $purpose = '', KeyRepositoryInterface $key_repository = NULL) {
    $library_config = [
      'debug' => (bool) $config
        ->get('debug_phpsaml'),
      'sp' => [
        'entityId' => $config
          ->get('sp_entity_id'),
        'assertionConsumerService' => [
          // See ExecuteInRenderContextTrait if curious why the long chained
          // call is necessary.
          'url' => Url::fromRoute('samlauth.saml_controller_acs', [], [
            'absolute' => TRUE,
          ])
            ->toString(TRUE)
            ->getGeneratedUrl(),
        ],
        'singleLogoutService' => [
          'url' => Url::fromRoute('samlauth.saml_controller_sls', [], [
            'absolute' => TRUE,
          ])
            ->toString(TRUE)
            ->getGeneratedUrl(),
        ],
        'NameIDFormat' => $config
          ->get('sp_name_id_format') ?: NULL,
      ],
      'idp' => [
        'entityId' => $config
          ->get('idp_entity_id'),
        'singleSignOnService' => [
          'url' => $config
            ->get('idp_single_sign_on_service'),
        ],
        'singleLogoutService' => [
          'url' => $config
            ->get('idp_single_log_out_service'),
        ],
      ],
      'security' => [
        // Used for metadata (adds a whole signature section):
        'signMetadata' => (bool) $config
          ->get('security_metadata_sign'),
        // Used for metadata (value goes into attribute
        // AuthnRequestsSigned="true/false" of SPSSODescriptor) / login(*):
        'authnRequestsSigned' => (bool) $config
          ->get('security_authn_requests_sign'),
        // Used for logout(*):
        'logoutRequestSigned' => (bool) $config
          ->get('security_logout_requests_sign'),
        // Used for SLO response, sent after processing incoming SLO request(*):
        'logoutResponseSigned' => (bool) $config
          ->get('security_logout_responses_sign'),
        // Used for logout; also influences Settings:__construct() checks for
        // presence of IDP cert:
        'nameIdEncrypted' => (bool) $config
          ->get('security_nameid_encrypt'),
        // Used for acs:
        // TRUE by default AND must be TRUE on existing installations that
        // didn't have the setting before, so it's the first one to get a
        // default value. (If we didn't have the (bool) operator, we wouldn't
        // necessarily need the default - but leaving it out would just invite
        // a bug later on.)
        'wantNameId' => (bool) ($config
          ->get('security_want_name_id') ?? TRUE),
        // Used for login / acs(*); indirectly influences metadata(**):
        'wantNameIdEncrypted' => (bool) $config
          ->get('security_nameid_encrypted'),
        // Used for acs(*); indirectly influences metadata(**):
        'wantAssertionsEncrypted' => (bool) $config
          ->get('security_assertions_encrypt'),
        // Used for metadata (value goes into attribute
        // WantAssertionsSigned="true/false" of SPSSODescriptor) / acs(*):
        'wantAssertionsSigned' => (bool) $config
          ->get('security_assertions_signed'),
        // Used for acs / sls (processing incoming SLO responses and requests):
        'wantMessagesSigned' => (bool) $config
          ->get('security_messages_sign'),
        // Used for login:
        'requestedAuthnContext' => (bool) $config
          ->get('security_request_authn_context'),
        // Used for login / logout / SLO response, sent after processing
        // incoming SLO request; should be deprecated:
        'lowercaseUrlencoding' => (bool) $config
          ->get('security_lowercase_url_encoding'),
      ],
      'strict' => (bool) $config
        ->get('strict'),
    ];
    $sig_alg = $config
      ->get('security_signature_algorithm');
    if ($sig_alg) {
      $library_config['security']['signatureAlgorithm'] = $sig_alg;
    }
    $enc_alg = $config
      ->get('security_encryption_algorithm');
    if ($enc_alg) {

      // Not a typo; this is snake_case in the library too.
      $library_config['security']['encryption_algorithm'] = $enc_alg;
    }
    if ($base_url) {
      $library_config['baseurl'] = $base_url;
    }

    // We want to read cert/key values from whereever they are stored, only
    // when we actually need them. This may lead to us creating a custom
    // \OneLogin\Saml2\Settings child class that contains the logic of 'just in
    // time' reading key/cert values from storage. However (leaving aside the
    // fact that we can't do that in v3.x because it will break compatibility),
    // - the Auth class cannot use Settings subclasses: its constructor only
    //   accepts an array and its $_settings variable is private (so we cannot
    //   get around this by subclassing Auth).
    // - while the design of Settings supports 'just in time reading', (and the
    //   Settings class even does it, in some cases), its setup _in principle_
    //   isn't well suited for handling that. So if we end up doing this, we'll
    //   need to add good comments about e.g. getSPData() not returning
    //   complete data, format*() not working, ... (Basically, my conclusion is
    //   like my conclusion elsewhere about the Auth object: from the design of
    //   the Settings object it isn't clear whether it wants to be a value
    //   object or whether it wants to contain the logic of how to read the
    //   key/cert values. It would be nice if the code was... clearer.)
    // So for now, we are adding logic to this method that 'knows' when the key
    // / certs are used.
    $add_key = $add_cert = $add_idp_cert = TRUE;
    $add_new_cert = in_array($purpose, [
      'metadata',
      '',
    ]);
    $add_idp_encryption_cert = FALSE;
    switch ($purpose) {
      case 'metadata':

        // signMetadata / wantNameIdEncrypted are not implemented yet but that
        // doesn't mean we can't already use them here: they potentially
        // influence metadata but then we can't turn off $add_key.
        $add_key = !empty($library_config['security']['signMetadata']) || !empty($library_config['security']['wantNameIdEncrypted']) || !empty($library_config['security']['wantAssertionsEncrypted']);

        // We cannot prevent Settings::checkIdPSettings() from being called
        // while using the standard Auth class, so cannot do this:
        // $add_idp_cert = FALSE;.
        // @todo this is a good enough reason for opening a PR to amend
        //   Auth::construct() (to accept a Settings that was constructed with
        //   (, TRUE)), regardless of considerations outlined in AuthVolatile.
        break;
      case 'login':

        // We don't need a cert for operation; we just need to set it to a
        // value if certain security settings are set (which need the private
        // key), or otherwise checkSPCerts() will freak out.
        $add_cert = !empty($library_config['security']['authnRequestsSigned']) || !empty($library_config['security']['wantNameIdEncrypted']) ? 'FAKE' : FALSE;

        // We cannot prevent Settings::checkIdPSettings() from being called
        // while using the standard Auth class, so cannot do this:
        // $add_idp_cert = FALSE;.
        break;
      case 'logout':

        // We don't need a cert for operation; we just need to set it to a
        // value if certain security settings are set (which need the private
        // key), or otherwise checkSPCerts() will freak out.
        $add_cert = !empty($library_config['security']['logoutRequestSigned']) ? 'FAKE' : FALSE;

        // We cannot prevent Settings::checkIdPSettings() from being called
        // while using the standard Auth class, so cannot do this:
        // $add_idp_cert=!empty($library_config['security']['nameIdEncrypted']);
        // ^ This would also need the 2nd parameter to Settings::__construct()
        // to be !$add_idp_cert.
        // That just means we should generally add at least 1 IdP cert, though.
        // We don't need to add the encryption cert specifically - only if:
        $add_idp_encryption_cert = !empty($library_config['security']['nameIdEncrypted']);
        break;
      case 'acs':

        // $add_key depends on the presence of any EncryptedAssertion elements,
        // which is too bothersome for us to check (and which is a reason to
        // have the key reading inside a child Settings object instead) so
        // we'll assume it's TRUE. Same with $add_idp_cert which depends on
        // the presence of various signed elements.
        $add_cert = !empty($library_config['security']['wantAssertionsEncrypted']) || !empty($library_config['security']['wantAssertionsSigned']) || !empty($library_config['security']['wantNameIdEncrypted']) ? 'FAKE' : FALSE;
        break;
      case 'sls-request':

        // We need to both interpret the incoming request and probably create a
        // new outgoing one, so we need key and cert.
        // We cannot prevent Settings::checkIdPSettings() from being called
        // while using the standard Auth class, so cannot do this:
        // $add_idp_cert = isset($_GET['Signature']); // This would also need
        // the 2nd parameter to Settings::__construct() to be !$add_idp_cert.
        break;
      case 'sls-response':
        $add_key = $add_cert = FALSE;
    }
    if (!$add_key || !$add_cert) {

      // If these are set, checkSPCerts() will get called, which requires key +
      // cert presence. Neither of these two variables are (should have been)
      // set to FALSE if any of these security settings is both true and
      // relevant for our $purpose. (The full logic including the switch{}
      // implies the library default is FALSE for all these security settings.)
      unset($library_config['security']['authnRequestsSigned']);
      unset($library_config['security']['logoutRequestSigned']);
      unset($library_config['security']['logoutResponseSigned']);
      unset($library_config['security']['wantAssertionsEncrypted']);
      unset($library_config['security']['wantNameIdEncrypted']);
    }
    if (!$add_idp_cert) {

      // Same for IdP cert.
      unset($library_config['security']['nameIdEncrypted']);
    }

    // Check if we want to load the certificates from a folder. Either folder
    // or cert+key settings should be defined. If both are defined, "folder" is
    // the preferred method and we ignore cert/path values; we don't do more
    // complicated validation like checking whether the cert/key files exist.
    // Initializing cert/key properties to '' (rather than not setting them at
    // all) should be fine, because the standard Settings also does that.
    $cert_folder = $config
      ->get('sp_cert_folder');
    if ($cert_folder) {

      // Set the folder so the SAML toolkit knows where to look. This is the
      // old method which only reads key/cert when we actually need it, because
      // the logic for it is inside the Settings class. We're phasing it out in
      // favor of the hardcoded above $add_* logic and reading the key/cert
      // files inside this function, because this old method relies on a
      // specific hardcoded folder name / file names in the Settings class.
      // @todo remove in 4.x: not applicable after samlauth_update_8304().
      if (!defined('ONELOGIN_CUSTOMPATH')) {
        define('ONELOGIN_CUSTOMPATH', "{$cert_folder}/");
      }
    }
    else {
      if ($add_key) {
        $key = $config
          ->get('sp_private_key');
        if (isset($key) && !is_string($key)) {
          throw new SamlError('SP private key setting is not a string.', SamlError::SETTINGS_INVALID);
        }
        $type = strstr($key, ':', TRUE);
        if ($type === 'key') {
          if ($key_repository) {
            $key = substr($key, 4);
            $key_entity = $key_repository
              ->getKey($key);
            if (!$key_entity) {
              throw new SamlError("SP private key '{$key}' not found.", SamlError::SETTINGS_INVALID);
            }
            $key = $key_entity
              ->getKeyValue();
          }
          else {
            throw new SamlError('SP private key setting is of type "key" but the Key module is not installed.', SamlError::SETTINGS_INVALID);
          }
        }
        elseif ($type === 'file') {
          $key = file_get_contents(substr($key, 5));
          if ($key === FALSE) {
            throw new SamlError('SP private key not found.', SamlError::PRIVATE_KEY_FILE_NOT_FOUND);
          }
        }
        $library_config['sp']['privateKey'] = $key;
      }
      if ($add_cert) {
        $cert = $add_cert === 'FAKE' ? 'dummy-value-to-subvert-validation' : $config
          ->get('sp_x509_certificate');
        if (isset($cert) && !is_string($cert)) {
          throw new SamlError('SP public cert setting is not a string.', SamlError::SETTINGS_INVALID);
        }
        if ($cert) {
          $type = strstr($cert, ':', TRUE);
          if ($type === 'key') {
            if ($key_repository) {
              $cert = substr($cert, 4);
              $key_entity = $key_repository
                ->getKey($cert);
              if (!$key_entity) {
                throw new SamlError("SP public cert '{$cert}' not found.", SamlError::SETTINGS_INVALID);
              }
              $cert = $key_entity
                ->getKeyValue();
            }
            else {
              throw new SamlError('SP public cert setting is of type "key" but the Key module is not installed.', SamlError::SETTINGS_INVALID);
            }
          }
          elseif ($type === 'file') {
            $cert = file_get_contents(substr($cert, 5));
            if ($cert === FALSE) {
              throw new SamlError('SP public cert not found.', SamlError::PUBLIC_CERT_FILE_NOT_FOUND);
            }
          }
          $library_config['sp']['x509cert'] = $cert;
        }
      }
      if ($add_new_cert) {
        $cert = $config
          ->get('sp_new_certificate');
        if (isset($cert) && !is_string($cert)) {
          throw new SamlError('SP new public cert setting is not a string.', SamlError::SETTINGS_INVALID);
        }
        if ($cert) {
          $type = strstr($cert, ':', TRUE);
          if ($type === 'key') {
            if ($key_repository) {
              $cert = substr($cert, 4);
              $key_entity = $key_repository
                ->getKey($cert);
              if (!$key_entity) {
                throw new SamlError("SP new public cert '{$cert}' not found.", SamlError::SETTINGS_INVALID);
              }
              $cert = $key_entity
                ->getKeyValue();
            }
            else {
              throw new SamlError('SP new public cert setting is of type "key" but the Key module is not installed.', SamlError::SETTINGS_INVALID);
            }
          }
          elseif ($type === 'file') {
            $cert = file_get_contents(substr($cert, 5));
            if ($cert === FALSE) {
              throw new SamlError('SP new public cert not found.', SamlError::PUBLIC_CERT_FILE_NOT_FOUND);
            }
          }
          $library_config['sp']['x509certNew'] = $cert;
        }
      }
    }
    $encryption_cert = '';
    $certs = [];
    if ($add_idp_encryption_cert) {
      $encryption_cert = $config
        ->get('idp_cert_encryption');
      if (isset($encryption_cert) && !is_string($encryption_cert)) {
        throw new SamlError('IdP encryption cert setting is not a string.', SamlError::SETTINGS_INVALID);
      }
      $type = strstr($encryption_cert, ':', TRUE);
      if ($type === 'key') {
        if ($key_repository) {
          $encryption_cert = substr($encryption_cert, 4);
          $key_entity = $key_repository
            ->getKey($encryption_cert);
          if (!$key_entity) {
            throw new SamlError("IdP encryption cert '{$encryption_cert}' not found.", SamlError::SETTINGS_INVALID);
          }
          $encryption_cert = $key_entity
            ->getKeyValue();
        }
        else {
          throw new SamlError('IdP encryption cert setting is of type "key" but the Key module is not installed.', SamlError::SETTINGS_INVALID);
        }
      }
      elseif ($type === 'file') {
        $encryption_cert = file_get_contents(substr($encryption_cert, 5));
        if ($encryption_cert === FALSE) {
          throw new SamlError('IdP encryption cert not found.', SamlError::PRIVATE_KEY_FILE_NOT_FOUND);
        }
      }
    }
    if ($add_idp_cert || $add_idp_encryption_cert && !$encryption_cert) {
      $certs = $config
        ->get('idp_certs');
      foreach ($certs as $i => $cert) {
        if (isset($certs[$i]) && !is_string($certs[$i])) {
          $nr = $i ? " {$i}" : '';
          throw new SamlError("IdP cert setting{$nr} is not a string.", SamlError::SETTINGS_INVALID);
        }
        $type = strstr($cert, ':', TRUE);
        if ($type === 'key') {
          if ($key_repository) {
            $cert = substr($cert, 4);
            $key_entity = $key_repository
              ->getKey($cert);
            if (!$key_entity) {
              throw new SamlError("IdP cert '{$cert}' not found.", SamlError::SETTINGS_INVALID);
            }
            $certs[$i] = $key_entity
              ->getKeyValue();
          }
          else {
            throw new SamlError('IdP cert setting is of type "key" but the Key module is not installed.', SamlError::SETTINGS_INVALID);
          }
        }
        elseif ($type === 'file') {
          $certs[$i] = file_get_contents(substr($cert, 5));
          if ($certs[$i] === FALSE) {
            $nr = $i ? " {$i}" : '';
            throw new SamlError("IdP cert{$nr} not found.", SamlError::PRIVATE_KEY_FILE_NOT_FOUND);
          }
        }
      }
    }

    // @todo remove in 4.x: not applicable after samlauth_update_8304().
    // @todo at the same time as removing this, uncomment samlauth_update_8400.
    if (!$certs && !$encryption_cert) {
      $old_cert = $config
        ->get('idp_x509_certificate');
      $old_cert_multi = $config
        ->get('idp_x509_certificate_multi');
      if ($old_cert || $old_cert_multi) {
        $certs = $old_cert ? [
          $old_cert,
        ] : [];
        if ($old_cert_multi) {
          if ($config
            ->get('idp_cert_type') === 'encryption') {
            $encryption_cert = $old_cert_multi;
          }
          else {
            $certs[] = $old_cert_multi;
          }
        }
      }
    }

    // If we don't set a separate 'x509certMulti > encryption' cert, the
    // 'main' cert (not 'x509certMulti > signing') is used for encryption so
    // it must be set. If we have a single 'main/signing' cert, we can set it
    // in either 'x509certMulti > signing' or as the main cert - both is not
    // necessary. This can be encoded in several arbitrary ways, e.g.:
    if ($encryption_cert) {
      $library_config['idp']['x509certMulti'] = [
        // This is an array, but the library never uses anything but the
        // first value.
        'encryption' => [
          $encryption_cert,
        ],
        'signing' => $certs,
      ];
    }
    elseif ($certs) {
      if (count($certs) == 1 || $add_idp_encryption_cert) {
        $library_config['idp']['x509cert'] = reset($certs);
      }
      if (count($certs) > 1) {
        $library_config['idp']['x509certMulti']['signing'] = $certs;
      }
    }
    return $library_config;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
SamlService::$authmap protected property The Authmap service.
SamlService::$configFactory protected property The config factory.
SamlService::$currentUser protected property The current user service.
SamlService::$entityTypeManager protected property The EntityTypeManager service.
SamlService::$eventDispatcher protected property The event dispatcher.
SamlService::$externalAuth protected property The ExternalAuth service.
SamlService::$flood protected property The flood service.
SamlService::$keyRepository protected property The Key repository service.
SamlService::$logger protected property A logger instance.
SamlService::$messenger protected property The messenger service.
SamlService::$privateTempStore protected property Private store for SAML session data.
SamlService::$requestStack protected property The request stack.
SamlService::$samlAuth protected property Auth objects (usually 0 or 1) representing the current request state.
SamlService::acs public function Processes a SAML response (Assertion Consumer Service).
SamlService::doLogin protected function Logs a user in, creating / linking an account; synchronizes attributes.
SamlService::drupalLogoutHelper protected function Ensures the user is logged out from Drupal; returns SAML session data.
SamlService::getAttributeByConfig public function Returns value from a SAML attribute whose name is configured in our module.
SamlService::getAttributes public function Returns all attributes in a SAML response.
SamlService::getMetadata public function Show metadata about the local sp. Use this to configure your saml2 IdP.
SamlService::getSamlAuth protected function Returns an initialized Auth class from the SAML Toolkit.
SamlService::linkExistingAccount protected function Link a pre-existing Drupal user to a given authname.
SamlService::login public function Initiates a SAML2 authentication flow and redirects to the IdP.
SamlService::logout public function Initiates a SAML2 logout flow and redirects to the IdP.
SamlService::processLoginResponse protected function Processes a SAML authentication response; throws an exception if invalid.
SamlService::reformatConfig protected static function Returns a configuration array as used by the external library.
SamlService::setKeyRepository public function Set the Key repository service.
SamlService::sls public function Does processing for the Single Logout Service.
SamlService::synchronizeUserAttributes public function Synchronizes user data with attributes in the SAML request.
SamlService::__construct public function Constructs a new SamlService.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
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.