saml_sp_drupal_login.module in SAML Service Provider 4.x
Same filename and directory in other branches
- 8.3 modules/saml_sp_drupal_login/saml_sp_drupal_login.module
- 8.2 modules/saml_sp_drupal_login/saml_sp_drupal_login.module
- 7.8 modules/saml_sp_drupal_login/saml_sp_drupal_login.module
- 7 modules/saml_sp_drupal_login/saml_sp_drupal_login.module
- 7.2 modules/saml_sp_drupal_login/saml_sp_drupal_login.module
- 7.3 modules/saml_sp_drupal_login/saml_sp_drupal_login.module
- 3.x modules/saml_sp_drupal_login/saml_sp_drupal_login.module
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.moduleView 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 = [];
$url_options = [];
if (isset($_GET['returnTo'])) {
$url_options['query']['returnTo'] = $_GET['returnTo'];
}
elseif (isset($_GET['destination'])) {
$url_options['query']['returnTo'] = $_GET['destination'];
}
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),
], $url_options);
$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,
], $url_options);
}
$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);
}
try {
$attributes = $saml_response
->getAttributes();
} catch (Exception $e) {
\Drupal::messenger()
->addError(t('An error occurred when parsing the response from %idp_label', [
'%idp_label' => $idp
->label(),
]));
\Drupal::logger('saml_sp')
->error('An error occurred when parsing the response from %idp_label: %exception', [
'%idp_label' => $idp
->label(),
'%exception' => $e
->__toString(),
]);
return new RedirectResponse($redirect_url);
}
// 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.
$email = '';
if ($idp
->getNameIdField() != 'mail') {
// Try to get email from SAML response attributes.
$mail_keys = [
'mail',
'urn:oid:0.9.2342.19200300.100.1.3',
];
foreach ($mail_keys as $key) {
if (!empty($attributes[$key]) && !empty($attributes[$key][0])) {
$email = $attributes[$key][0];
break;
}
}
if (!$email) {
// TODO: Either fail completely here or add tests for email in the
// cases below where it is required.
\Drupal::logger('saml_sp')
->warning('No mail attribute available; please check IdP %idp_label configuration.', [
'%idp_label' => $idp
->label(),
]);
}
}
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
/**/
}
/**
* 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
Name | 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_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(). |