You are here

public function UserSyncEventSubscriber::onUserSync in SAML Authentication 4.x

Same name and namespace in other branches
  1. 8.3 src/EventSubscriber/UserSyncEventSubscriber.php \Drupal\samlauth\EventSubscriber\UserSyncEventSubscriber::onUserSync()
  2. 8.2 src/EventSubscriber/UserSyncEventSubscriber.php \Drupal\samlauth\EventSubscriber\UserSyncEventSubscriber::onUserSync()

Performs actions to synchronize users with SAML data on login.

Parameters

\Drupal\samlauth\Event\SamlauthUserSyncEvent $event: The event.

File

src/EventSubscriber/UserSyncEventSubscriber.php, line 125

Class

UserSyncEventSubscriber
Event subscriber that synchronizes user properties on a user_sync event.

Namespace

Drupal\samlauth\EventSubscriber

Code

public function onUserSync(SamlauthUserSyncEvent $event) {

  // If the account is new, we are in the middle of a user save operation;
  // the current user name is the authname as set by externalauth, and
  // e-mail is not set yet.
  $account = $event
    ->getAccount();
  $fatal_errors = [];

  // Synchronize username.
  // @todo in v4, can/should we get rid of most of this validation code, and
  //   just call $account->validate() afterwards? (Because that supposedly
  //   also checks for duplicate e-mail addresses etc.) This should be in
  //   'base' code, likely moved into externalauth if possible. (It's
  //   mentioned in #3132453. At the moment I think we should have an option
  //   for entity level validation, and keep field level validation in here.)
  if ($account
    ->isNew() || $this->config
    ->get('sync_name')) {

    // Get value from the SAML attribute whose name is configured in the
    // samlauth module.
    $name = $this
      ->getAttributeByConfig('user_name_attribute', $event);
    if ($name && $name != $account
      ->getAccountName()) {

      // Validate the username. This shouldn't be necessary to mitigate
      // attacks; assuming our SAML setup is correct, noone can insert fake
      // data here. It protects against SAML attribute misconfigurations.
      // Invalid names will cancel the login / account creation. The code is
      // copied from user_validate_name().
      $definition = BaseFieldDefinition::create('string')
        ->addConstraint('UserName', []);
      $data = $this->typedDataManager
        ->create($definition);
      $data
        ->setValue($name);
      $violations = $data
        ->validate();
      if ($violations) {
        foreach ($violations as $violation) {
          $fatal_errors[] = $violation
            ->getMessage();
        }
      }

      // Check if the username is not already taken by someone else. For new
      // accounts this can happen if the 'map existing users' setting is off.
      if (!$fatal_errors) {
        $account_search = $this->entityTypeManager
          ->getStorage('user')
          ->loadByProperties([
          'name' => $name,
        ]);
        $existing_account = reset($account_search);
        if (!$existing_account || $account
          ->id() == $existing_account
          ->id()) {
          $account
            ->setUsername($name);
          $event
            ->markAccountChanged();
        }
        else {
          $error = 'An account with the username @username already exists.';
          if ($account
            ->isNew()) {
            $fatal_errors[] = $this
              ->t($error, [
              '@username' => $name,
            ]);
          }
          else {

            // We continue and keep the old name. A DSM should be OK here
            // since login only happens interactively.
            $error = "Error updating user name from SAML attribute: {$error}";
            $this->logger
              ->error($error, [
              '@username' => $name,
            ]);
            $this->messenger
              ->addError($this
              ->t($error, [
              '@username' => $name,
            ]));
          }
        }
      }
    }
  }

  // Synchronize e-mail.
  if ($this->config
    ->get('user_mail_attribute') && ($account
    ->isNew() || $this->config
    ->get('sync_mail'))) {
    $mail = $this
      ->getAttributeByConfig('user_mail_attribute', $event);
    if ($mail) {
      if ($mail != $account
        ->getEmail()) {

        // Invalid e-mail cancels the login / account creation just like name.
        if ($this->emailValidator
          ->isValid($mail)) {
          $account
            ->setEmail($mail);
          if ($account
            ->isNew()) {

            // Externalauth sets init to a non e-mail value so we will fix it.
            $account
              ->set('init', $mail);
          }
          $event
            ->markAccountChanged();
        }
        else {
          $fatal_errors[] = $this
            ->t('Invalid e-mail address @mail', [
            '@mail' => $mail,
          ]);
        }
      }
    }
    elseif ($account
      ->isNew() && !$account
      ->getEmail()) {

      // We won't allow new accounts with empty e-mail. If a custom event
      // subscriber wants to populate the e-mail, then (at least for now) it
      // should be registered with a higher priority than this standard one.
      $fatal_errors[] = $this
        ->t('Email address is not provided in SAML attribute.');
    }
  }
  if ($fatal_errors) {

    // Cancel the whole login process and/or account creation.
    throw new \RuntimeException('Error(s) encountered during SAML attribute synchronization: ' . implode(' // ', $fatal_errors));
  }
}