You are here

public function SamlService::acs in SAML Authentication 4.x

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. 8.2 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.

Return value

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

File

src/SamlService.php, line 274

Class

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

Namespace

Drupal\samlauth

Code

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;
}