You are here

saml_sp_drupal_login.module in SAML Service Provider 3.x

SAML Drupal Login.

Uses the SAML Service Provider module to provide a Drupal-login authentication module.

File

modules/saml_sp_drupal_login/saml_sp_drupal_login.module
View source
<?php

/**
 * @file
 * SAML Drupal Login.
 *
 * Uses the SAML Service Provider module to provide a Drupal-login
 * authentication module.
 */
use Drupal\Core\Render\Element;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\saml_sp\Entity\Idp;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use OneLogin\Saml2\Response;

/*
// Used by commented code in function saml_sp_user_logout():
use OneLogin\Saml2\LogoutRequest;
use OneLogin\Saml2\Settings;
use OneLogin\Saml2\Utils;
/**/

/**
 * Implements hook_form_FORM_ID_alter().
 */
function saml_sp_drupal_login_form_user_form_alter(&$form, $form_state, $form_id) {
  $config = \Drupal::config('saml_sp_drupal_login.config');
  $user = \Drupal::currentUser();
  if ($config
    ->get('force_saml_only') && !$user
    ->hasPermission('administer users')) {
    hide($form['account']['mail']);
    hide($form['account']['pass']);
    hide($form['account']['current_pass_required_values']);
    hide($form['account']['current_pass']);
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function saml_sp_drupal_login_form_user_login_form_alter(&$form, $form_state, $form_id) {
  $config = \Drupal::config('saml_sp_drupal_login.config');
  $idps = $config
    ->get('idp');
  $enabled_idps = [];
  if (!empty($idps)) {
    foreach ($idps as $key => $value) {
      if ($value) {
        $enabled_idps[$key] = $key;
      }
    }
  }
  if (empty($enabled_idps)) {

    // There are no enabled IdPs, so we aren't doing anything to the form.
    return;
  }
  if ($config
    ->get('force_saml_only')) {

    // Disable caching of the login page.
    \Drupal::service('page_cache_kill_switch')
      ->trigger();

    // Only SAML logins are accepted, so don't show the form.
    foreach (Element::children($form) as $key) {
      $form[$key]['#access'] = FALSE;
    }
    if (count($enabled_idps) == 1) {

      // There is only one IdP so redirect to its login page to remove one step.
      $redirect_url = Url::fromRoute('saml_sp_drupal_login.login', [
        'idp' => array_shift($enabled_idps),
      ]);
      $response = new RedirectResponse($redirect_url
        ->toString());
      $response
        ->send();
    }
  }
  $idps = saml_sp__load_all_idps();
  $links = [];
  foreach ($enabled_idps as $value) {
    if (empty($idps[$value])) {
      continue;
    }
    $links[] = Link::createFromRoute(t('Login to @site_name using %idp.', [
      '@site_name' => \Drupal::config('system.site')
        ->get('name'),
      '%idp' => $idps[$value]
        ->label(),
    ]), 'saml_sp_drupal_login.login', [
      'idp' => $value,
    ]);
  }
  $form['saml_sp_drupal_login_links'] = [
    '#theme' => 'item_list',
    '#items' => $links,
  ];
  $form['#cache']['tags'] = isset($form['#cache']['tags']) ? array_merge($form['#cache']['tags'], $config
    ->getCacheTags()) : $config
    ->getCacheTags();
}

/**
 * SAML authentication callback.
 */
function saml_sp_drupal_login__saml_authenticate($is_valid, Response $saml_response, Idp $idp) {
  $redirect_url = $_POST['RelayState'] ?: Url::fromRoute('<front>')
    ->toString();
  if (!$is_valid) {
    \Drupal::messenger()
      ->addError(t('Could not authenticate via %idp_label', [
      '%idp_label' => $idp
        ->label(),
    ]));
    \Drupal::logger('saml_sp')
      ->warning('Could not authenticate via %idp_label', [
      '%idp_label' => $idp
        ->label(),
    ]);
    return new RedirectResponse($redirect_url);
  }
  $attributes = $saml_response
    ->getAttributes();

  // Get the NameID value from response.
  $name_id = $saml_response
    ->getNameId();
  if (\Drupal::config('saml_sp.settings')
    ->get('debug')) {
    _saml_sp__debug('Response NameId', $name_id);
  }

  // If email address is not used to identify user,
  // it has to be in the attributes.
  if ($idp
    ->getNameIdField() != 'mail') {

    // Try to get email from SAML response attributes.
    try {
      $email = $attributes['mail'][0];
    } catch (Exception $e) {
      \Drupal::logger('saml_sp')
        ->error('No mail attribute available; please check IdP %idp_label configuration. Exception message: %exception', [
        '%idp_label' => $idp
          ->label(),
        '%exception' => $e->message,
      ]);

      // TODO: should we give up here and not allow authentication?
    }
  }
  else {
    $email = $name_id;
  }
  $site_register_access = \Drupal::config('user.settings')
    ->get('register');
  $config = \Drupal::config('saml_sp_drupal_login.config');
  $success = FALSE;
  if ($user = saml_sp_drupal_login_get_user($name_id, $idp
    ->getNameIdField(), $email)) {

    // Successful login to existing user account.
    $success = TRUE;
  }
  elseif ($site_register_access == UserInterface::REGISTER_VISITORS) {

    // Successful authentication, but no user account.
    // New users are allowed to register.
    $language = \Drupal::languageManager()
      ->getCurrentLanguage()
      ->getId();
    $user = User::create();

    // Mandatory:
    $user
      ->setPassword(random_bytes(64));
    $user
      ->enforceIsNew();
    $user
      ->setEmail($email);
    $user
      ->setUsername($email);

    // Optional:
    $user
      ->set('init', $email);
    $user
      ->set('langcode', $language);
    $user
      ->set('preferred_langcode', $language);
    $user
      ->set('preferred_admin_langcode', $language);

    /*
        $user->set('setting_name', 'setting_value');
        $user->addRole('rid');
        /**/

    // Activate and save user account.
    $user
      ->activate();
    $result = $user
      ->save();
    \Drupal::logger('saml_sp')
      ->notice('New SSO user account for %mail with UID %uid.', [
      '%mail' => $email,
      '%uid' => $user
        ->id(),
    ]);
    $success = TRUE;
  }
  elseif ($config
    ->get('no_account_authenticated_user_role') && $config
    ->get('no_account_authenticated_user_account')) {

    // Successful authentication, but no user account.
    // The setting allows for them to get an authenticated role.
    $user = User::load($config
      ->get('no_account_authenticated_user_account'));
    if (empty($user)) {
      \Drupal::messenger()
        ->addError(t('You have been authenticated but there is no account available for you to continue logging in. Please contact a site administrator.'));
      \Drupal::logger('saml_sp')
        ->notice('User authenticated via %idp_label with email %mail, cannot grant access to generic account as the generic account could not be loaded.', [
        '%idp_label' => $idp
          ->label(),
        '%mail' => $email,
      ]);
      $success = FALSE;
    }
    else {
      \Drupal::logger('saml_sp')
        ->notice('User authenticated via %idp_label with email %mail, granted access to %name account.', [
        '%idp_label' => $idp
          ->label(),
        '%mail' => $email,
        '%name' => $user
          ->getAccountName(),
      ]);
      $success = TRUE;
    }
  }
  else {

    // Successful authentication, but no user account.
    $_SESSION['authenticated_via_saml_sp'] = TRUE;
    $tokens = [
      '%mail' => $email,
      '%idp_label' => $idp
        ->label(),
    ];
    $rvaa = $site_register_access == UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL;
    $arra = $config
      ->get('account_request_request_account');
    if (!$rvaa && !$arra) {

      // Only administrators can register new users.
      $no_account_message = t('No account matching %mail has been found. Please contact a site administrator.', $tokens);
      \Drupal::messenger()
        ->addWarning($no_account_message);
    }
    else {

      // The user is allowed to request an account from administrators.
      // Do not create an account, and redirect to the registration page.
      if ($rvaa) {

        // User is allowed to request by account settings.
        $registration_route = 'user.register';
      }
      else {

        // User is allowed to request by SAML SP Drupal Login settings.
        $registration_route = 'saml_sp_drupal_login.register';
      }
      \Drupal::messenger()
        ->addWarning(t('This site requires you to request an account.'));
      $redirect_url = Url::fromRoute($registration_route, [], [
        'query' => [
          'email' => $email,
        ],
      ])
        ->toString();
    }
    \Drupal::logger('saml_sp')
      ->warning("User attempting to login through %idp_label with %mail which doesn't match any accounts.", $tokens);
  }
  if ($success) {

    // @see user_login_name_validate().
    if ($user
      ->isBlocked() || !$user
      ->isActive()) {
      \Drupal::messenger()
        ->addError(t('The username %name has not been activated or is blocked.', [
        '%name' => $user
          ->getAccountName(),
      ]));
      if (\Drupal::config('saml_sp.settings')
        ->get('debug')) {
        _saml_sp__debug('Account', $this);
        _saml_sp__debug('Response NameId', $name_id);
      }
    }
    else {

      // TODO: this might not be the right place for this. It doesn't do
      // anything right now anyway.
      saml_sp_drupal_login_update_user_attributes($user, $email, $attributes);
      \Drupal::logger('saml_sp')
        ->notice('User %name logging in through SAML via %idp_name. with NameID %mail', [
        '%name' => $user
          ->getAccountName(),
        '%idp_name' => $idp
          ->label(),
        '%mail' => $email,
      ]);

      // Store the fact that the user logged in via the SAML SP module.
      $_SESSION['authenticated_via_saml_sp'] = TRUE;
      user_login_finalize($user);
    }
  }
  return new RedirectResponse($redirect_url);
}

/**
 * Return whether the user is currently authenticated by the SAML SP module.
 *
 * @return bool
 *   TRUE if the user is currently authenticated.
 */
function saml_sp_drupal_login_is_authenticated() {
  return isset($_SESSION['authenticated_via_saml_sp']) && $_SESSION['authenticated_via_saml_sp'] === TRUE;
}

/**
 * Get the User object from either users table or custom field.
 *
 * Custom field should be used if the users need to be able to change the email
 * address on IdP, because then it cannot be used for identifying a user.
 * Email address can be used as a backup method if user is singing in for the
 * first time and their NameID value has not been stored to the given field yet.
 *
 * @param string $name_id
 *   The NameID value which SSO server provides in SAML response.
 * @param string $field_name
 *   The name of the field in Drupal where NameID is stored.
 * @param string $email
 *   User email address which is only used if NameID cannot be found.
 *
 * @return \Drupal\user\UserInterface|false
 *   The user object in Drupal which matches the NameID or email address, or
 *   FALSE if it cannot be found.
 */
function saml_sp_drupal_login_get_user($name_id, $field_name, $email = NULL) {
  if ($field_name == 'mail') {
    return user_load_by_mail($name_id);
  }

  // Find the uid from the field where it is supposed to be stored.
  $db_field = 'field_data_' . $field_name;
  $column = $field_name . '_value';
  $uid = \Drupal::database()
    ->select($db_field, 'nameid')
    ->fields('nameid', [
    'entity_id',
  ])
    ->condition($column, $name_id, '=')
    ->execute()
    ->fetchField();

  // If uid is not found, try to find it from the users table with the email.
  // This might be the case if existing users are exported to new IdP,
  // then they will not have ID from IdP on their first login.
  $update_name_id = FALSE;
  if (empty($uid)) {
    $uid = \Drupal::database()
      ->query("SELECT uid FROM {users} WHERE mail = :mail", [
      ':mail' => $email,
    ])
      ->fetchField();
    $update_name_id = TRUE;
  }
  if (empty($uid)) {
    return FALSE;
  }

  // We found a user; update if necessary and return.
  $user = User::load($uid);
  if ($update_name_id) {
    $wrapper = entity_metadata_wrapper('user', $user);
    $wrapper->field_nameid
      ->set($name_id);
    $wrapper
      ->save();
  }
  return $user;
}

/**
 * Implements hook_user_logout().
 */
function saml_sp_user_logout($account) {

  /*
    // @codingStandardsIgnoreStart
    // Load the IdP to authenticate against.
    $idp = saml_sp_drupal_login__get_id();

    // what is the authentication method?
    switch ($idp->getAuthnContextClassRef()) {
      case 'urn:federation:authentication:windows':
        // the user is logged in through their Windows account
        // it is impractical to log out of the IdP system as well
        return;
        break;
    }

    if (!variable_get('saml_sp_drupal_login__logout', TRUE)) {
      // the site doesn't want the IdP to be signed out of,
      // so just log out of Drupal
      return;
    }
    global $language;
    global $base_url;


    // Settings is an array
    $settings = saml_sp__get_settings($idp);
    // Creating Saml2 Settings object from array
    $saml_settings = new Settings($settings);
    $idp_data = $saml_settings->getIdPData();

    // Checking if logout url is configured
    if (isset($idp_data['singleLogoutService']) && isset($idp_data['singleLogoutService']['url'])) {
      $slo_url = $idp_data['singleLogoutService']['url'];
    }
    else {
      throw new Exception("The IdP does not support Single Log Out");
    }

    // Creating a logout request to be passed to IdP
    if (isset($_SESSION['IdPSessionIndex']) && !empty($_SESSION['IdPSessionIndex'])) {
      $logout_request = new LogoutRequest($saml_settings, NULL, NULL ,$_SESSION['IdPSessionIndex']);
    }
    else {
      $logout_request = new LogoutRequest($saml_settings);
    }

    $saml_request = $logout_request->getRequest();
    $parameters = array('SAMLRequest' => $saml_request);
    // Checking current language, so that user can be redirected to front page
    // in same language
    $parameters['RelayState'] = $base_url . '/' . $language->prefix;
    $url = Utils::redirect($slo_url, $parameters, TRUE);
    \Drupal::logger('saml_sp')->notice('Session closed for %name (%uid) and starting SAML SLO.', array('%name' => $account->name, '%uid' => $account->uid));
    // Force redirection in drupal_goto().
    unset($_GET['destination']);
    if(!empty($saml_request)) {
      drupal_goto($url);
    }
    // @codingStandardsIgnoreEnd
    /**/
}

/**
 * Implements hook_mail().
 */
function saml_sp_drupal_login_mail($key, &$message, $params) {
  $langcode = $message['langcode'];
  switch ($key) {
    case 'account_request':
      $params[':site_name'] = \Drupal::config('system.site')
        ->get('name') ?: Url::fromRoute('<front>')
        ->toString();
      $message['subject'] = t('Account request for :site_name.', $params, [
        'langcode' => $langcode,
      ]);
      $message['body'][] = t(':name would like an account set up on :site_name using the e-mail address :mail.', $params, [
        'langcode' => $langcode,
      ]);
      $message['body'][] = t('The explanation given is:', $params, [
        'langcode' => $langcode,
      ]);
      $message['body'][] = t(':explanation', $params, [
        'langcode' => $langcode,
      ]);
      break;
  }
}

/**
 * Updates user attributes from SAML data after successful login.
 *
 * @param \Drupal\user\UserInterface $user
 *   The logged-in user.
 * @param string $email
 *   The user's email address.
 * @param array $attributes
 *   Other attributes returned from the IdP.
 *
 * @TODO: All of it.
 */
function saml_sp_drupal_login_update_user_attributes(UserInterface $user, $email, array $attributes) {

  // Default language is the site default.
  $language = \Drupal::languageManager()
    ->getCurrentLanguage()
    ->getId();

  // If language attribute is set on IdP, then use that language.
  if (isset($attributes['language'])) {
    $language = $attributes['language'][0];
  }

  /*
    // @codingStandardsIgnoreStart
    // Update email address if it has changed on IdP.
    if (\Drupal::config('saml_sp_drupal_login.config')->get('update_email') && $user->mail != $email) {
      \Drupal::logger('saml_sp')->notice('Updating email address from %old_email to %new_email for UID %uid', array('%old_email' => $user->mail, '%new_email' => $email, '%uid' => $user->uid));
      $wrapper = entity_metadata_wrapper('user', $user);
      $wrapper->mail->set($email);
      $wrapper->save();
      // Showing message for user about the update which happened on IdP.
      $message = t('Your email address is now @new_email', array('@new_email' => $email));
      \Drupal::messenger()->addMessage($message);
    }
    // Update language if it has changed on IdP.
    if (\Drupal::config('saml_sp_drupal_login.config')->get('update_language') && $account->language != $language) {
      \Drupal::logger('saml_sp')->notice('Updating language from %old_lang to %new_lang for UID %uid', array('%old_lang' => $user->language, '%new_lang' => $language, '%uid' => $user->uid));
      $wrapper = entity_metadata_wrapper('user', $user);
      $wrapper->language->set($language);
      $wrapper->save();
    }
    // @codingStandardsIgnoreEnd
    /**/
}

Functions

Namesort descending Description
saml_sp_drupal_login_form_user_form_alter Implements hook_form_FORM_ID_alter().
saml_sp_drupal_login_form_user_login_form_alter Implements hook_form_FORM_ID_alter().
saml_sp_drupal_login_get_user Get the User object from either users table or custom field.
saml_sp_drupal_login_is_authenticated Return whether the user is currently authenticated by the SAML SP module.
saml_sp_drupal_login_mail Implements hook_mail().
saml_sp_drupal_login_update_user_attributes Updates user attributes from SAML data after successful login.
saml_sp_drupal_login__saml_authenticate SAML authentication callback.
saml_sp_user_logout Implements hook_user_logout().