You are here

public function SamlService::acs in SAML Authentication 8.2

Same name and namespace in other branches
  1. 8.3 src/SamlService.php \Drupal\samlauth\SamlService::acs()
  2. 8 src/SamlService.php \Drupal\samlauth\SamlService::acs()
  3. 4.x src/SamlService.php \Drupal\samlauth\SamlService::acs()

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.

Throws

Exception

File

src/SamlService.php, line 166

Class

SamlService
Governs communication between the SAML toolkit and the IDP / login behavior.

Namespace

Drupal\samlauth

Code

public function acs() {

  // This call can either set an error condition or throw a
  // \OneLogin_Saml2_Error exception, depending on whether or not we are
  // processing a POST request. Don't catch the exception.
  $this
    ->getSamlAuth()
    ->processResponse();

  // Now look if there were any errors and also throw.
  $errors = $this
    ->getSamlAuth()
    ->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 ACS response. Type(s): ' . implode(', ', array_unique($errors)) . '; reason given for last error: ' . $this
      ->getSamlAuth()
      ->getLastErrorReason());
  }
  if (!$this
    ->isAuthenticated()) {
    throw new RuntimeException('Could not authenticate.');
  }
  $unique_id = $this
    ->getAttributeByConfig('unique_id_attribute');
  if (!$unique_id) {
    throw new Exception('Configured unique ID is not present in SAML response.');
  }
  $account = $this->externalAuth
    ->load($unique_id, 'samlauth');
  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 e-mail.
    if ($this->config
      ->get('map_users')) {
      $event = new SamlauthUserLinkEvent($this
        ->getAttributes());
      $this->eventDispatcher
        ->dispatch(SamlauthEvents::USER_LINK, $event);
      $account = $event
        ->getLinkedAccount();
      if (!$account) {

        // The linking by name / e-mail cannot be bypassed at this point
        // because it makes no sense to create a new account from the SAML
        // attributes if one of these two basic properties is already in use.
        // (In this case a newly created and logged-in account would get a
        // cryptic machine name because  synchronizeUserAttributes() cannot
        // assign the proper name while saving.)
        $name = $this
          ->getAttributeByConfig('user_name_attribute');
        if ($name && ($account_search = $this->entityTypeManager
          ->getStorage('user')
          ->loadByProperties([
          'name' => $name,
        ]))) {
          $account = reset($account_search);
          $this->logger
            ->info('Matching local user @uid found for name @name (as provided in a SAML attribute); associating user and logging in.', [
            '@name' => $name,
            '@uid' => $account
              ->id(),
          ]);
        }
        else {
          $mail = $this
            ->getAttributeByConfig('user_mail_attribute');
          if ($mail && ($account_search = $this->entityTypeManager
            ->getStorage('user')
            ->loadByProperties([
            'mail' => $mail,
          ]))) {
            $account = reset($account_search);
            $this->logger
              ->info('Matching local user @uid found for e-mail @mail (as provided in a SAML attribute); associating user and logging in.', [
              '@mail' => $mail,
              '@uid' => $account
                ->id(),
            ]);
          }
        }
      }
    }
    if ($account) {

      // There is a chance that the following call will not actually link the
      // account (if a mapping to this account already exists from another
      // unique ID). If that happens, it does not matter much to us; we will
      // just log the account in anyway. Next time the same not-yet-linked
      // user logs in, we will again try to link the account in the same way
      // and (falsely) log that we are associating the user.
      $this->externalAuth
        ->linkExistingAccount($unique_id, 'samlauth', $account);
    }
  }

  // If we haven't found an account to link, create one from the SAML
  // attributes.
  if (!$account) {
    if ($this->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 = $this->externalAuth
        ->register($unique_id, 'samlauth');
      $this->externalAuth
        ->userLoginFinalize($account, $unique_id, 'samlauth');
    }
    else {
      throw new RuntimeException('No existing user account matches the SAML ID provided. This authentication service is not configured to create new accounts.');
    }
  }
  elseif ($account
    ->isBlocked()) {
    throw new RuntimeException('Requested account is blocked.');
  }
  else {

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

  // Set some request properties in local private storage. We can use these on
  // logout.
  foreach ([
    'session_index' => $this->samlAuth
      ->getSessionIndex(),
    'session_expiration' => $this->samlAuth
      ->getSessionExpiration(),
    'name_id' => $this->samlAuth
      ->getNameId(),
    'name_id_format' => $this->samlAuth
      ->getNameIdFormat(),
  ] as $key => $value) {
    if (isset($value)) {
      $this->privateTempStore
        ->set($key, $value);
    }
    else {
      $this->privateTempStore
        ->delete($key);
    }
  }
}