samlauth.module in SAML Authentication 7
Same filename and directory in other branches
Provides SAML authentication capabilities.
File
samlauth.moduleView 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
Name | 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. |