You are here

samlauth.module in SAML Authentication 7

Provides SAML authentication capabilities.

File

samlauth.module
View source
<?php

/**
 * @file
 * Provides SAML authentication capabilities.
 */

/**
 * Implements hook_menu().
 */
function samlauth_menu() {
  $items = array();
  $items['admin/config/people/saml'] = array(
    'title' => 'SAML Authentication',
    'description' => 'Configure SAML authentication behaviors.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'samlauth_configure_form',
    ),
    'access arguments' => array(
      'configure saml',
    ),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'samlauth.admin.inc',
  );
  $items['saml/metadata'] = array(
    'title' => 'SAML Metadata',
    'page callback' => 'samlauth_metadata',
    'access arguments' => array(
      'view sp metadata',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['saml/login'] = array(
    'title' => 'SAML Login',
    'page callback' => 'samlauth_login',
    'access callback' => 'user_is_anonymous',
    'type' => MENU_CALLBACK,
  );
  $items['saml/logout'] = array(
    'title' => 'SAML Logout',
    'page callback' => 'samlauth_logout',
    // The logout process can always be started.
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items['saml/acs'] = array(
    'title' => 'SAML ACS',
    'page callback' => 'samlauth_acs',
    'access callback' => 'user_is_anonymous',
    'type' => MENU_CALLBACK,
  );
  $items['saml/sls'] = array(
    'title' => 'SAML SLS',
    'page callback' => 'samlauth_sls',
    'access callback' => 'user_is_anonymous',
    'type' => MENU_CALLBACK,
  );
  $items['saml/changepw'] = array(
    'title' => 'SAML Change Password',
    'page callback' => 'samlauth_changepw',
    'access callback' => 'user_is_logged_in',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function samlauth_permission() {
  return array(
    'view sp metadata' => array(
      'title' => t('View SP metadata'),
      'description' => t('View the SP metadata XML at /saml/metadata'),
    ),
    'configure saml' => array(
      'title' => t('Configure SAML'),
      'description' => t('Configure SAML authentication behaviors'),
    ),
  );
}

/**
 * Implements hook_form_FORM_ID_alter for user_login.
 *
 * @param $form
 * @param $form_state
 */
function samlauth_form_user_login_alter(&$form, $form_state) {
  samlauth_login_form_alter($form, $form_state);
}

/**
 * Implements hook_form_FORM_ID_alter for user_login_block.
 *
 * @param $form
 * @param $form_state
 */
function samlauth_form_user_login_block_alter(&$form, $form_state) {
  samlauth_login_form_alter($form, $form_state);
}

/**
 * Implements hook_form_FORM_ID_alter for user_pass (password reset).
 *
 * @param $form
 * @param $form_state
 */
function samlauth_form_user_pass_alter(&$form, $form_state) {
  samlauth_login_form_alter($form, $form_state);
}

/**
 * Common callback for user_login_block / user_login / user_pass.
 */
function samlauth_login_form_alter(&$form, $form_state) {
  $form['#validate'][] = 'samlauth_check_saml_user';
}

/**
 * Validation callback for SAML users logging in through the normal login form.
 */
function samlauth_check_saml_user($form, $form_state) {
  if (!variable_get('samlauth_drupal_saml_login', FALSE)) {
    if (form_get_errors()) {

      // If previous validation functions have already failed (name/pw incorrect
      // or blocked), bail out so we don't disclose any details about a user that
      // otherwise wouldn't be authenticated.
      return;
    }
    $account = user_load_by_name($form_state['values']['name']);
    if (!$account) {
      $account = user_load_by_mail($form_state['values']['name']);
    }
    if (!$account) {
      form_set_error('name', t('Could not load user to do a validation check.'));
    }
    else {
      $saml_id = samlauth_find_unique_id_by_user_id($account->uid);
      if ($saml_id !== FALSE) {
        form_set_error('name', t('SAML users must sign in with SSO.'));
      }
    }
  }
}

/**
 * Returns configuration array for SAML SP.
 *
 * @return array
 */
function samlauth_get_config() {
  $config = array(
    'baseurl' => url('saml', array(
      'absolute' => TRUE,
    )) . '/',
    'strict' => (bool) variable_get('samlauth_security_strict'),
    'sp' => array(
      'entityId' => variable_get('samlauth_sp_entity_id'),
      'assertionConsumerService' => array(
        'url' => url('saml/acs', array(
          'absolute' => TRUE,
        )),
      ),
      'singleLogoutService' => array(
        'url' => url('saml/sls', array(
          'absolute' => TRUE,
        )),
      ),
      'NameIDFormat' => variable_get('samlauth_sp_name_id_format'),
      'x509cert' => variable_get('samlauth_sp_x509_certificate'),
      'privateKey' => variable_get('samlauth_sp_private_key'),
    ),
    'idp' => array(
      'entityId' => variable_get('samlauth_idp_entity_id'),
      'singleSignOnService' => array(
        'url' => variable_get('samlauth_idp_single_sign_on_service'),
      ),
      'singleLogoutService' => array(
        'url' => variable_get('samlauth_idp_single_log_out_service'),
      ),
      'x509cert' => variable_get('samlauth_idp_x509_certificate'),
    ),
    'security' => array(
      'authnRequestsSigned' => (bool) variable_get('samlauth_security_authn_requests_sign'),
      'logoutRequestSigned' => (bool) variable_get('samlauth_logout_requests_sign'),
      'logoutResponseSigned' => (bool) variable_get('samlauth_logout_responses_sign'),
      'wantMessagesSigned' => (bool) variable_get('samlauth_security_messages_sign'),
      'wantAssertionsSigned' => (bool) variable_get('samlauth_security_assertions_signed'),
      'wantAssertionsEncrypted' => (bool) variable_get('samlauth_security_assertions_encrypted'),
      'wantNameId' => (bool) variable_get('samlauth_want_name_id', TRUE),
      'requestedAuthnContext' => (bool) variable_get('samlauth_security_request_authn_context'),
      'lowercaseUrlencoding' => (bool) variable_get('samlauth_lowercase_url_encoding'),
    ),
  );

  // Passing NULL for signatureAlgorithm would be OK, but not ''.
  $sig_alg = variable_get('samlauth_security_security_signature_algorithm');
  if ($sig_alg) {
    $config['security']['signatureAlgorithm'] = $sig_alg;
  }
  return $config;
}

/**
 * Get an instance of the SAML Auth class.
 *
 * @return \OneLogin\Saml2\Auth
 *   The SAML Auth library object.
 */
function samlauth_get_saml2_auth() {
  return new OneLogin\Saml2\Auth(samlauth_get_config());
}

/**
 * Menu callback for /saml/metadata.
 */
function samlauth_metadata() {
  $auth = samlauth_get_saml2_auth();
  $settings = $auth
    ->getSettings();
  $metadata = $settings
    ->getSPMetadata();
  $errors = $settings
    ->validateMetadata($metadata);
  if (!empty($errors)) {
    throw new Exception('Invalid SP metadata: ' . implode(', ', $errors));
  }
  drupal_add_http_header('Content-Type', 'text/xml');

  // @TODO: This is gross. Investigate using a delivery callback instead.
  print $metadata;
  drupal_exit();
}

/**
 * Menu callback for /saml/login.
 */
function samlauth_login() {
  $auth = samlauth_get_saml2_auth();
  $auth
    ->login('/', array(), FALSE, FALSE, FALSE, variable_get('samlauth_set_nameidpolicy', TRUE));
}

/**
 * Menu callback for /saml/logout.
 */
function samlauth_logout() {
  $nameId = isset($_SESSION['samlauth']['nameId']) ? $_SESSION['samlauth']['nameId'] : NULL;
  $nameIdFormat = isset($_SESSION['samlauth']['nameIdFormat']) ? $_SESSION['samlauth']['nameIdFormat'] : NULL;
  $nameIdNameQualifier = isset($_SESSION['samlauth']['nameIdNameQualifier']) ? $_SESSION['samlauth']['nameIdNameQualifier'] : NULL;
  $sessionIndex = isset($_SESSION['samlauth']['sessionIndex']) ? $_SESSION['samlauth']['sessionIndex'] : NULL;
  if (user_is_logged_in()) {
    module_load_include('pages.inc', 'user');
    user_logout_current_user();
  }

  // Check if there is a SAML response already. We have no way of verifying the
  // response because SAML PHP processResponse() does not support logout
  // responses and processSlo() only looks at GET parameters.
  if (isset($_POST['SAMLResponse'])) {

    // Just assume everything went well and go to the frontpage.
    drupal_goto();
  }
  else {

    // Redirect to the auth server for logout.
    $auth = samlauth_get_saml2_auth();
    $auth
      ->logout('/', array(), $nameId, $sessionIndex, FALSE, $nameIdFormat, $nameIdNameQualifier);
  }
}

/**
 * Menu callback for /saml/sls.
 */
function samlauth_sls() {
  $url = '';
  try {

    // There are two valid cases here:
    // - We're processing a LogoutRequest, which will return a URL to redirect
    //   to (which is the IdP); the RelayState is not for us.
    // - We're processing a LogoutResponse, which will return NULL; the
    //   RelayState is meant for us to process.
    $auth = samlauth_get_saml2_auth();
    $url = $auth
      ->processSLO(FALSE, NULL, (bool) variable_get('samlauth_logout_reuse_sigs'), NULL, TRUE);
    $errors = $auth
      ->getErrors();
    if (empty($errors)) {
      if (!$url) {

        // We should be able to trust the RelayState parameter at this point
        // because the response from the IDP was verified.
        if (isset($_REQUEST['RelayState'])) {
          $url = $_REQUEST['RelayState'];
        }
      }

      // Log out the user, since the logout already happened on the server.
      if (user_is_logged_in()) {
        module_load_include('pages.inc', 'user');
        user_logout_current_user();
      }
    }
    else {
      drupal_set_message('SLS error: ' . implode(', ', $errors), 'error');
    }
  } catch (Exception $e) {
    drupal_set_message('SLS error: ' . $e
      ->getMessage(), 'error');
  }
  drupal_goto($url ?: '<front>');
}

/**
 * Menu callback for /saml/changepw.
 */
function samlauth_changepw() {
  $changepw_service = variable_get('samlauth_changepw_service', FALSE);
  if ($changepw_service === FALSE) {
    drupal_set_message('The site administrator has not configured a password change service. Please change your password according to the normal processes in your organization.', 'error');
    drupal_goto('/');
  }
  drupal_goto(variable_get('samlauth_changepw_service'), array(
    'external' => TRUE,
  ));
}

/**
 * Menu callback for /saml/acs.
 */
function samlauth_acs() {
  $account = FALSE;
  try {
    $auth = samlauth_get_saml2_auth();
    $auth
      ->processResponse();
    $errors = $auth
      ->getErrors();
    if (!empty($errors)) {
      $reason = $auth
        ->getLastErrorReason();

      // Special handling for passive authentication requests: if they fail it
      // means the user is not logged in, so redirect them to our login form.
      if ($reason == 'The status code of the Response was not Success, was Responder -> Cannot authenticate Subject in Passive Mode') {
        watchdog('samlauth', 'Passive authentication failed, redirecting to login form. @reason', [
          '@reason' => $reason,
        ], WATCHDOG_INFO);
        drupal_goto('user/login', array(
          'query' => array(
            'saml' => 'anonymous',
          ),
        ));
      }
      else {
        throw new Exception('ACS error: ' . implode(', ', $errors) . " Reason: {$reason}");
      }
    }
    if (!$auth
      ->isAuthenticated()) {
      throw new Exception('SAML user is not authenticated.');
    }

    // Get the attributes returned by the IdP.
    $saml_data = $auth
      ->getAttributes();

    // First, let's see if we have a user.
    $unique_id_attribute = variable_get('samlauth_unique_id_attribute', 'eduPersonTargetedID');

    // If the configured unique id attribute is not present, then bail out early.
    if (!isset($saml_data[$unique_id_attribute][0])) {
      watchdog('samlauth', 'Configured unique ID is not present in the SAML response: <pre>@response</pre>', array(
        '@response' => print_r($saml_data, TRUE),
      ), WATCHDOG_ERROR);
      throw new Exception('Configured unique ID is not present in the SAML response.');
    }

    // Try to find the user using the ID that we are given.
    $unique_id = $saml_data[$unique_id_attribute][0];
    $uid = samlauth_find_uid_by_unique_id($unique_id);
    if ($uid) {

      // Load the user that was found.
      $account = user_load($uid);

      // If the account is blocked, bail out.
      if ($account->status != 1) {
        throw new Exception("User account is blocked.");
      }
    }
    else {
      $mail_attribute = variable_get('samlauth_user_mail_attribute', 'email');
      if (variable_get('samlauth_map_users_email', FALSE) && ($account = user_load_by_mail($saml_data[$mail_attribute]))) {
        samlauth_associate_saml_id_with_account($unique_id, $account);
      }
      elseif (variable_get('samlauth_create_users', FALSE)) {
        $account = samlauth_create_user_from_saml_data($saml_data);
      }
      else {
        throw new Exception('No existing user account matches the SAML ID provided. This authentication service is not configured to create new accounts.');
      }
    }
  } catch (Exception $e) {
    drupal_set_message($e
      ->getMessage(), 'error');
    watchdog_exception('samlauth', $e);
    drupal_goto();
  }

  // If we're good to continue, log the user in.
  global $user;
  $user = $account;
  user_login_finalize();

  // Store the authentication details in the user's session, we need it later
  // when logging out.
  $_SESSION['samlauth']['nameId'] = $auth
    ->getNameId();
  $_SESSION['samlauth']['nameIdFormat'] = $auth
    ->getNameIdFormat();
  $_SESSION['samlauth']['nameIdNameQualifier'] = $auth
    ->getNameIdNameQualifier();
  $_SESSION['samlauth']['sessionIndex'] = $auth
    ->getSessionIndex();
  drupal_goto();
}

/**
 * Find a user given the unique ID from the SAML IdP.
 *
 * @param $id
 *
 * @return int
 *   Returns the uid of the user with the unique ID.
 */
function samlauth_find_uid_by_unique_id($id) {
  return db_select('authmap', 'a')
    ->fields('a', array(
    'uid',
  ))
    ->condition('authname', $id)
    ->condition('module', 'samlauth')
    ->execute()
    ->fetchField();
}

/**
 * Find a unique id from a username.
 */
function samlauth_find_unique_id_by_user_id($uid) {
  return db_select('authmap', 'a')
    ->fields('a', array(
    'authname',
  ))
    ->condition('module', 'samlauth')
    ->condition('uid', $uid)
    ->execute()
    ->fetchField();
}

/**
 * Ensure that a SAML ID is associated with a given user account.
 *
 * @param $saml_id
 * @param \Drupal\Core\Session\AccountInterface $account
 */
function samlauth_associate_saml_id_with_account($saml_id, $account) {
  $uid = samlauth_find_uid_by_unique_id($saml_id);
  if ($uid == NULL) {
    db_insert('authmap')
      ->fields(array(
      'uid' => $account->uid,
      'authname' => $saml_id,
      'module' => 'samlauth',
    ))
      ->execute();
  }
  else {
    db_update('authmap')
      ->fields(array(
      'uid' => $account->uid,
      'authname' => $saml_id,
      'module' => 'samlauth',
    ))
      ->condition('uid', $account->uid)
      ->condition('module', 'samlauth')
      ->execute();
  }
}

/**
 * Create a new user from SAML response data.
 *
 * @param $saml_data
 */
function samlauth_create_user_from_saml_data($saml_data) {
  $user_unique_attribute = variable_get('samlauth_unique_id_attribute');
  $user_name_attribute = variable_get('samlauth_user_name_attribute');
  $user_mail_attribute = variable_get('samlauth_user_mail_attribute');
  if (!isset($saml_data[$user_name_attribute][0])) {
    throw new Exception('Missing name attribute in SAML response.');
  }
  if (!isset($saml_data[$user_mail_attribute][0])) {
    throw new Exception('Missing mail attribute in SAML response.');
  }
  $account = new stdClass();
  $account->name = $saml_data[$user_name_attribute][0];
  $account->pass = user_password(50);
  $account->mail = $saml_data[$user_mail_attribute][0];
  $account->status = 1;

  // Fix timezone warning when creating new user.
  $account->timezone = variable_get('date_default_timezone', 0);

  // Allow other modules to change/set user properties before saving.
  drupal_alter('samlauth_new_user', $account, $saml_data);
  user_save($account);
  samlauth_associate_saml_id_with_account($saml_data[$user_unique_attribute][0], $account);
  return $account;
}

Functions

Namesort descending Description
samlauth_acs Menu callback for /saml/acs.
samlauth_associate_saml_id_with_account Ensure that a SAML ID is associated with a given user account.
samlauth_changepw Menu callback for /saml/changepw.
samlauth_check_saml_user Validation callback for SAML users logging in through the normal login form.
samlauth_create_user_from_saml_data Create a new user from SAML response data.
samlauth_find_uid_by_unique_id Find a user given the unique ID from the SAML IdP.
samlauth_find_unique_id_by_user_id Find a unique id from a username.
samlauth_form_user_login_alter Implements hook_form_FORM_ID_alter for user_login.
samlauth_form_user_login_block_alter Implements hook_form_FORM_ID_alter for user_login_block.
samlauth_form_user_pass_alter Implements hook_form_FORM_ID_alter for user_pass (password reset).
samlauth_get_config Returns configuration array for SAML SP.
samlauth_get_saml2_auth Get an instance of the SAML Auth class.
samlauth_login Menu callback for /saml/login.
samlauth_login_form_alter Common callback for user_login_block / user_login / user_pass.
samlauth_logout Menu callback for /saml/logout.
samlauth_menu Implements hook_menu().
samlauth_metadata Menu callback for /saml/metadata.
samlauth_permission Implements hook_permission().
samlauth_sls Menu callback for /saml/sls.