Commit b8ddb24b authored by Richard Mansfield's avatar Richard Mansfield Committed by Gerrit Code Review
Browse files

Merge changes Ib93680e2,I5321c027,Ia8a79939

* changes:
  Redevelop auth/saml - self-linking accounts, dual login
  Enable multiple auth_remote_user connections
  Add SAML based SSO Login link
parents ec13abf0 93358ed3
...@@ -244,15 +244,42 @@ function edituser_site_validate(Pieform $form, $values) { ...@@ -244,15 +244,42 @@ function edituser_site_validate(Pieform $form, $values) {
} }
} }
// Check that the external username isn't already in use // Check that the external username isn't already in use by someone else
if (isset($values['remoteusername']) && if (isset($values['authinstance']) && isset($values['remoteusername'])) {
$usedby = get_record_select('auth_remote_user', // there are 4 cases for changes on the page
'authinstance = ? AND remoteusername = ? AND localusr != ?', // 1) ai and remoteuser have changed
array($values['authinstance'], $values['remoteusername'], $values['id'])) // 2) just ai has changed
) { // 3) just remoteuser has changed
$usedbyuser = get_field('usr', 'username', 'id', $usedby->localusr); // 4) the ai changes and the remoteuser is wiped - this is a delete of the old ai-remoteuser
$SESSION->add_error_msg(get_string('duplicateremoteusername', 'auth', $usedbyuser));
$form->set_error('remoteusername', get_string('duplicateremoteusernameformerror', 'auth')); // determine the current remoteuser
$current_remotename = get_field('auth_remote_user', 'remoteusername',
'authinstance', $user->authinstance, 'localusr', $user->id);
if (!$current_remotename) {
$current_remotename = $user->username;
}
// what should the new remoteuser be
$new_remoteuser = get_field('auth_remote_user', 'remoteusername',
'authinstance', $values['authinstance'], 'localusr', $user->id);
if (!$new_remoteuser) {
$new_remoteuser = $user->username;
}
if (strlen(trim($values['remoteusername'])) > 0) {
// value changed on page - use it
if ($values['remoteusername'] != $current_remotename) {
$new_remoteuser = $values['remoteusername'];
}
}
// what really counts is who owns the target remoteuser slot
$target_owner = get_field('auth_remote_user', 'localusr',
'authinstance', $values['authinstance'], 'remoteusername', $new_remoteuser);
// target remoteuser is owned by someone else
if ($target_owner && $target_owner != $user->id) {
$usedbyuser = get_field('usr', 'username', 'id', $target_owner);
$SESSION->add_error_msg(get_string('duplicateremoteusername', 'auth', $usedbyuser));
$form->set_error('remoteusername', get_string('duplicateremoteusernameformerror', 'auth'));
}
} }
} }
...@@ -292,34 +319,55 @@ function edituser_site_submit(Pieform $form, $values) { ...@@ -292,34 +319,55 @@ function edituser_site_submit(Pieform $form, $values) {
} }
set_account_preference($user->id, 'maildisabled', $values['maildisabled']); set_account_preference($user->id, 'maildisabled', $values['maildisabled']);
// Authinstance can be changed by institutional admins if both the // process the change of the authinstance and or the remoteuser
// old and new authinstances belong to the admin's institutions if (isset($values['authinstance']) && isset($values['remoteusername'])) {
$remotename = get_field('auth_remote_user', 'remoteusername', 'authinstance', $user->authinstance, 'localusr', $user->id); // Authinstance can be changed by institutional admins if both the
if (!$remotename) { // old and new authinstances belong to the admin's institutions
$remotename = $user->username; $authinst = get_records_select_assoc('auth_instance', 'id = ? OR id = ?',
}
if (isset($values['authinstance'])
&& ($values['authinstance'] != $user->authinstance
|| (isset($values['remoteusername']) && $values['remoteusername'] != $remotename))) {
$authinst = get_records_select_assoc('auth_instance', 'id = ? OR id = ?',
array($values['authinstance'], $user->authinstance)); array($values['authinstance'], $user->authinstance));
if ($USER->get('admin') || if ($USER->get('admin') ||
($USER->is_institutional_admin($authinst[$values['authinstance']]->institution) && ($USER->is_institutional_admin($authinst[$values['authinstance']]->institution) &&
$USER->is_institutional_admin($authinst[$user->authinstance]->institution))) { $USER->is_institutional_admin($authinst[$user->authinstance]->institution))) {
delete_records('auth_remote_user', 'localusr', $user->id); // determine the current remoteuser
$current_remotename = get_field('auth_remote_user', 'remoteusername',
'authinstance', $user->authinstance, 'localusr', $user->id);
if (!$current_remotename) {
$current_remotename = $user->username;
}
// if the remoteuser is empty and the ai has changed - delete the old remoteuser record
if (strlen(trim($values['remoteusername'])) == 0 &&
$values['authinstance'] != $user->authinstance) {
delete_records('auth_remote_user', 'authinstance', $user->authinstance, 'localusr', $user->id);
}
// we do not create a remoteuser record for internal ai's
if ($authinst[$values['authinstance']]->authname != 'internal') { if ($authinst[$values['authinstance']]->authname != 'internal') {
if (isset($values['remoteusername']) && strlen($values['remoteusername']) > 0) { // what should the new remoteuser be
$un = $values['remoteusername']; $new_remoteuser = get_field('auth_remote_user', 'remoteusername',
'authinstance', $values['authinstance'], 'localusr', $user->id);
// save the remotename for the target existence check
$target_remotename = $new_remoteuser;
if (!$new_remoteuser) {
$new_remoteuser = $user->username;
}
if (strlen(trim($values['remoteusername'])) > 0) {
// value changed on page - use it
if ($values['remoteusername'] != $current_remotename) {
$new_remoteuser = $values['remoteusername'];
}
} }
else { // only update remote name if the input actually changed on the page or it doesn't yet exist
$un = $remotename; if ($current_remotename != $new_remoteuser || !$target_remotename) {
// only remove the ones related to this traget authinstance as we now allow multiple
// for dual login mechanisms
delete_records('auth_remote_user', 'authinstance', $values['authinstance'], 'localusr', $user->id);
insert_record('auth_remote_user', (object) array(
'authinstance' => $values['authinstance'],
'remoteusername' => $new_remoteuser,
'localusr' => $user->id,
));
} }
insert_record('auth_remote_user', (object) array(
'authinstance' => $values['authinstance'],
'remoteusername' => $un,
'localusr' => $user->id,
));
} }
// update the ai on the user master
$user->authinstance = $values['authinstance']; $user->authinstance = $values['authinstance'];
// update the global $authobj to match the new authinstance // update the global $authobj to match the new authinstance
...@@ -331,7 +379,7 @@ function edituser_site_submit(Pieform $form, $values) { ...@@ -331,7 +379,7 @@ function edituser_site_submit(Pieform $form, $values) {
// Only change the pw if the new auth instance allows for it // Only change the pw if the new auth instance allows for it
if (method_exists($authobj, 'change_password')) { if (method_exists($authobj, 'change_password')) {
$user->passwordchange = (int) ($values['passwordchange'] == 'on'); $user->passwordchange = (int) (isset($values['passwordchange']) && $values['passwordchange'] == 'on' ? 1 : 0);
if (isset($values['password']) && $values['password'] !== '') { if (isset($values['password']) && $values['password'] !== '') {
$userobj = new User(); $userobj = new User();
......
...@@ -522,7 +522,7 @@ function uploadcsv_submit(Pieform $form, $values) { ...@@ -522,7 +522,7 @@ function uploadcsv_submit(Pieform $form, $values) {
log_debug('added user ' . $user->username); log_debug('added user ' . $user->username);
} }
else if (isset($UPDATES[$user->username])) { else if (isset($UPDATES[$user->username])) {
$updated = update_user($user, $profilefields, $remoteuser, $values); $updated = update_user($user, $profilefields, $remoteuser, $values, true);
if (empty($updated)) { if (empty($updated)) {
// Nothing changed for this user // Nothing changed for this user
......
...@@ -1016,6 +1016,9 @@ function auth_get_login_form() { ...@@ -1016,6 +1016,9 @@ function auth_get_login_form() {
'register' => array( 'register' => array(
'value' => '<div id="login-helplinks">' . '<a href="' . get_config('wwwroot') . 'forgotpass.php" tabindex="2">' . get_string('lostusernamepassword') . '</a></div>' 'value' => '<div id="login-helplinks">' . '<a href="' . get_config('wwwroot') . 'forgotpass.php" tabindex="2">' . get_string('lostusernamepassword') . '</a></div>'
), ),
'loginsaml' => array(
'value' => ((count_records('auth_instance', 'authname', 'saml') == 0) ? '' : '<div id="login-helplinks"><a href="' . get_config('wwwroot') . 'auth/saml/" tabindex="2">' . get_string('login', 'auth.saml') . '</a></div>')
),
'login_submitted' => array( 'login_submitted' => array(
'type' => 'hidden', 'type' => 'hidden',
'value' => 1 'value' => 1
...@@ -1568,7 +1571,10 @@ function auth_generate_login_form() { ...@@ -1568,7 +1571,10 @@ function auth_generate_login_form() {
'register' => array( 'register' => array(
'value' => '<div id="login-helplinks">' . $registerlink 'value' => '<div id="login-helplinks">' . $registerlink
. '<a href="' . get_config('wwwroot') . 'forgotpass.php" tabindex="2">' . get_string('lostusernamepassword') . '</a></div>' . '<a href="' . get_config('wwwroot') . 'forgotpass.php" tabindex="2">' . get_string('lostusernamepassword') . '</a></div>'
) ),
'loginsaml' => array(
'value' => ((count_records('auth_instance', 'authname', 'saml') == 0) ? '' : '<a href="' . get_config('wwwroot') . 'auth/saml/" tabindex="2">' . get_string('login', 'auth.saml') . '</a>')
),
) )
))); )));
......
<?php <?php
/** /**
* Mahara: Electronic portfolio, weblog, resume builder and social networking * Mahara: Electronic portfolio, weblog, resume builder and social networking
* Copyright (C) 2006-2009 Catalyst IT Ltd (http://www.catalyst.net.nz) * Copyright (C) 2006-2009 Catalyst IT Ltd (http://www.catalyst.net.nz)
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
* @subpackage auth-saml * @subpackage auth-saml
* @author Piers Harding <piers@catalyst.net.nz> * @author Piers Harding <piers@catalyst.net.nz>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL * @license http://www.gnu.org/copyleft/gpl.html GNU GPL
* @copyright (C) 2006-2009 Catalyst IT Ltd http://catalyst.net.nz * @copyright (C) 2006-2011 Catalyst IT Ltd http://catalyst.net.nz
* *
* This file incorporates work covered by the following copyright and * This file incorporates work covered by the following copyright and
* permission notice: * permission notice:
...@@ -45,20 +45,20 @@ ...@@ -45,20 +45,20 @@
define('INTERNAL', 1); define('INTERNAL', 1);
define('PUBLIC', 1); define('PUBLIC', 1);
define('SAML_RETRIES', 5);
global $CFG, $USER, $SESSION; global $CFG, $USER, $SESSION;
require(dirname(dirname(dirname(__FILE__))) . '/init.php');
require_once(get_config('docroot') .'auth/saml/lib.php');
require_once(get_config('libroot') .'institution.php');
// do our own partial initialisation so that we can get at the config // check that the plugin is active
// this version of init.php has the user session initiation stuff ripped out if (get_field('auth_installed', 'active', 'name', 'saml') != 1) {
// this is because SimpleSAMLPHP does all kinds of things with the PHP session redirect();
// handling including changing the cookie names etc. }
require(dirname(__FILE__) . '/init.php');
// get the config pointing to the SAML library - and load it // get the config pointing to the SAML library - and load it
$samllib = get_config_plugin('auth', 'saml', 'simplesamlphplib'); $samllib = get_config_plugin('auth', 'saml', 'simplesamlphplib');
if (null === $samllib) { if (!file_exists($samllib.'/lib/_autoload.php')) {
exit(0); throw new AuthInstanceException(get_string('errorbadssphplib','auth.saml'));
} }
require_once($samllib.'/lib/_autoload.php'); require_once($samllib.'/lib/_autoload.php');
...@@ -71,7 +71,7 @@ SimpleSAML_Configuration::init($samlconfig); ...@@ -71,7 +71,7 @@ SimpleSAML_Configuration::init($samlconfig);
$saml_session = SimpleSAML_Session::getInstance(); $saml_session = SimpleSAML_Session::getInstance();
// do we have a logout request? // do we have a logout request?
if(param_variable("logout", false)) { if (param_variable("logout", false)) {
// logout the saml session // logout the saml session
$sp = $saml_session->getAuthority(); $sp = $saml_session->getAuthority();
if (! $sp) { if (! $sp) {
...@@ -85,35 +85,44 @@ if (! in_array($sp, SimpleSAML_Auth_Source::getSources())) { ...@@ -85,35 +85,44 @@ if (! in_array($sp, SimpleSAML_Auth_Source::getSources())) {
$sp = 'default-sp'; $sp = 'default-sp';
} }
$as = new SimpleSAML_Auth_Simple($sp); $as = new SimpleSAML_Auth_Simple($sp);
// Check the SimpleSAMLphp config is compatible
$saml_config = SimpleSAML_Configuration::getInstance(); $saml_config = SimpleSAML_Configuration::getInstance();
$session_handler = $saml_config->getString('session.handler', false);
if (!$session_handler || $session_handler == 'phpsession') {
throw new AuthInstanceException(get_string('errorbadssphp','auth.saml'));
}
// what is the session like?
$valid_saml_session = $saml_session->isValid($sp); $valid_saml_session = $saml_session->isValid($sp);
// do we have a returnto URL ? // figure out what the returnto URL should be
$wantsurl = param_variable("wantsurl", false); $wantsurl = param_variable("wantsurl", false);
if($wantsurl) { if (!$wantsurl) {
$_SESSION['wantsurl'] = $wantsurl; if (isset($_SESSION['wantsurl'])) {
} $wantsurl = $_SESSION['wantsurl'];
else if (isset($_SESSION['wantsurl'])) { }
$wantsurl = $_SESSION['wantsurl']; else if (! $saml_session->getIdP()) {
} $wantsurl = array_key_exists('HTTP_REFERER',$_SERVER) ? $_SERVER['HTTP_REFERER'] : $CFG->wwwroot;
else if (! $saml_session->getIdP()){ }
$_SESSION['wantsurl'] = array_key_exists('HTTP_REFERER',$_SERVER) ? $_SERVER['HTTP_REFERER'] : $CFG->wwwroot; else {
$wantsurl = $_SESSION['wantsurl']; $wantsurl = $CFG->wwwroot;
} }
else {
$wantsurl = $CFG->wwwroot;
} }
// taken from Moodle clean_param // taken from Moodle clean_param - make sure the wantsurl is correctly formed
include_once('validateurlsyntax.php'); include_once('validateurlsyntax.php');
if (!validateUrlSyntax($wantsurl, 's?H?S?F?E?u-P-a?I?p?f?q?r?')) { if (!validateUrlSyntax($wantsurl, 's?H?S?F?E?u-P-a?I?p?f?q?r?')) {
$wantsurl = $CFG->wwwroot; $wantsurl = $CFG->wwwroot;
} }
// trim off any reference to login and stash
$_SESSION['wantsurl'] = preg_replace('/\&login$/', '', $wantsurl);
// now - are we logged in? // now - are we logged in?
$as->requireAuth(); $as->requireAuth();
// ensure that $_SESSION is cleared // ensure that $_SESSION is cleared for simplesamlphp
if (isset($_SESSION['wantsurl'])) { if (isset($_SESSION['wantsurl'])) {
unset($_SESSION['wantsurl']); unset($_SESSION['wantsurl']);
} }
...@@ -136,156 +145,212 @@ require_once(dirname(dirname(dirname(__FILE__))) . '/auth/lib.php'); ...@@ -136,156 +145,212 @@ require_once(dirname(dirname(dirname(__FILE__))) . '/auth/lib.php');
$SESSION = Session::singleton(); $SESSION = Session::singleton();
$USER = new LiveUser(); $USER = new LiveUser();
$THEME = new Theme($USER); $THEME = new Theme($USER);
// The installer does its own auth_setup checking, because some upgrades may // ***********************************************************************
// break logging in and so need to allow no logins. // END of copied stuff from original init.php
if (!defined('INSTALLER')) { // ***********************************************************************
auth_setup(); // restart the session for Mahara
@session_start();
if (!$SESSION->get('wantsurl')) {
$SESSION->set('wantsurl', preg_replace('/\&login$/', '', $wantsurl));
} }
if (get_config('siteclosed')) { // now start the hunt for the associated authinstance for the organisation attached to the saml_attributes
if ($USER->admin) { global $instance;
if (get_config('disablelogin')) { $instance = auth_saml_find_authinstance($saml_attributes);
$USER->logout();
} // if we don't have an auth instance then this is a serious failure
else if (!defined('INSTALLER')) { if (!$instance) {
redirect('/admin/upgrade.php'); throw new UserNotFoundException(get_string('errorbadinstitution','auth.saml'));
}
}
if (!$USER->admin) {
if ($USER->is_logged_in()) {
$USER->logout();
}
if (!defined('HOME') && !defined('INSTALLER')) {
redirect();
}
}
} }
// check to see if we're installed... // stash the existing logged in user - if we have one
if (!get_config('installed')) { $current_user = $USER;
ensure_install_sanity(); $is_loggedin = $USER->is_logged_in();
$scriptfilename = str_replace('\\', '/', $_SERVER['SCRIPT_FILENAME']); // check the instance and do a test login
if (false === strpos($scriptfilename, 'admin/index.php') $can_login = false;
&& false === strpos($scriptfilename, 'admin/upgrade.php') try {
&& false === strpos($scriptfilename, 'admin/upgrade.json.php')) { $auth = new AuthSaml($instance->id);
redirect('/admin/'); $can_login = $auth->request_user_authorise($saml_attributes);
} }
catch (AccessDeniedException $e) {
throw new UserNotFoundException(get_string('errnosamluser','auth.saml'));
}
catch (XmlrpcClientException $e) {
throw new AccessDeniedException($e->getMessage());
}
catch (AuthInstanceException $e) {
throw new AccessDeniedException(get_string('errormissinguserattributes', 'auth.saml'));
} }
if (defined('JSON') && !defined('NOSESSKEY')) { // if we can login with SAML - then let them go
$sesskey = param_variable('sesskey', null); if ($can_login) {
global $USER; // they are logged in, so they dont need to be here
if ($sesskey === null || $USER->get('sesskey') != $sesskey) { if ($SESSION->get('wantsurl')) {
$USER->logout(); $wantsurl = $SESSION->get('wantsurl');
json_reply('global', get_string('invalidsesskey'), 1); $SESSION->set('wantsurl', null);
} }
session_write_close();
redirect($wantsurl);
} }
// ***********************************************************************
// END of copied stuff from original init.php
// ***********************************************************************
// are we configured to allow testing of local login and linking?
$loginlink = get_field('auth_instance_config', 'value', 'field', 'loginlink', 'instance', $instance->id);
if (empty($loginlink)) {
throw new UserNotFoundException(get_string('errnosamluser','auth.saml'));
}
// restart the session for Mahara // used in the submit callback for auth_saml_loginlink_screen()
@session_start(); global $remoteuser;
$user_attribute = get_field('auth_instance_config', 'value', 'field', 'user_attribute', 'instance', $instance->id);
$remoteuser = $saml_attributes[$user_attribute][0];
require_once(get_config('docroot') .'auth/saml/lib.php'); // is the local account already logged in or can the SAML auth succeed - if not try to get
require_once(get_config('libroot') .'institution.php'); // them to log in local/manual
if (!$is_loggedin) {
// cannot match user account - so offer them the login-link/register page
// if we can't login locally, and cant login via SAML then we should offer to register - but this should probably appear on the local login page anyway
auth_saml_login_screen($remoteuser);
}
else {
// if we can login locally, but can't login with SAML then we offer to link the accounts SAML -> local one
auth_saml_loginlink_screen($remoteuser, $current_user->username);
}
exit(0);
// if the user is not logged in, then lets start it going
if(!$USER->is_logged_in()) { /**
simplesaml_init($saml_config, $valid_saml_session, $saml_attributes, $as); * callback for linking local account with remote SAML account
*
* @param Pieform $form
* @param array $values
*/
function auth_saml_loginlink_submit(Pieform $form, $values) {
global $USER, $instance, $remoteuser;
// create the new account linking
db_begin();
delete_records('auth_remote_user', 'authinstance', $instance->id, 'localusr', $USER->id);
insert_record('auth_remote_user', (object) array(
'authinstance' => $instance->id,
'remoteusername' => $remoteuser,
'localusr' => $USER->id,
));
db_commit();
session_write_close();
redirect('/auth/saml/');
} }
// they are logged in, so they dont need to be here
// header('Location: '.$CFG->wwwroot);
redirect($wantsurl);
/** /**
* check the validity of the users current SAML 2.0 session * Find the connected authinstance for the organisation attached to this SAML account
* if its bad, force log them out of Mahara, and redirect them to the IdP
* if it's good, find an applicable saml auth instance, and try logging them in with it
* passing in the attributes found from the IdP
* *
* @param object $saml_config saml configuration object * @param array $saml_attributes
* @param boolean $valid_saml_session is there a valid saml2 session *
* @param array $saml_attributes saml attributes passed in by the IdP * @return object authinstance record
* @param object $as new saml user object
* @return nothing
*/ */
function simplesaml_init($saml_config, $valid_saml_session, $saml_attributes, $as) { function auth_saml_find_authinstance($saml_attributes) {
global $CFG, $USER, $SESSION; // find the one (it should be only one) that has the right field, and the right field value for institution
$instance = false;
// $idp = get_config_plugin('auth', 'saml', 'idpidentity'); $institutions = array();
$retry = $SESSION->get('retry');
if ($retry > SAML_RETRIES) {
throw new AccessTotallyDeniedException(get_string('errorretryexceeded','auth.saml', $retry));
}
else if (!$valid_saml_session) { #
if ($USER->is_logged_in()) {
$USER->logout();
}
$SESSION->set('messages', array());
$SESSION->set('retry', $retry + 1);
// not valid session. Ship user off to the Identity Provider
$as->requireAuth();
} else {
// find all the possible institutions/auth instances
$instances = recordset_to_array(get_recordset_sql("SELECT * FROM {auth_instance_config} aic, {auth_instance} ai WHERE ai.id = aic.instance AND ai.authname = 'saml' AND aic.field = 'institutionattribute'"));
// find the one (it should be only one) that has the right field, and the right field value for institution
$instance = false;
$institutions = array();
foreach ($instances as $row) {
$institutions[]= $row->instance.':'.$row->institution.':'.$row->value;
if (isset($saml_attributes[$row->value])) {
// does this institution use a regex match against the institution check value?
if ($configvalue = get_record('auth_instance_config', 'instance', $row->instance, 'field', 'institutionregex')) {
$is_regex = (boolean) $configvalue->value;
}
else {
$is_regex = false;
}
if ($configvalue = get_record('auth_instance_config', 'instance', $row->instance, 'field', 'institutionvalue')) {
$institution_value = $configvalue->value;
}
else {
$institution_value = $row->institution;
}
if ($is_regex) { // find all the possible institutions/auth instances of type saml
foreach ($saml_attributes[$row->value] as $attr) { $instances = recordset_to_array(get_recordset_sql("SELECT * FROM {auth_instance_config} aic, {auth_instance} ai WHERE ai.id = aic.instance AND ai.authname = 'saml' AND aic.field = 'institutionattribute'"));
if (preg_match('/'.trim($institution_value).'/', $attr)) { foreach ($instances as $row) {
$instance = $row; $institutions[]= $row->instance.':'.$row->institution.':'.$row->value;
break; if (isset($saml_attributes[$row->value])) {
} // does this institution use a regex match against the institution check value?
} if ($configvalue = get_record('auth_instance_config', 'instance', $row->instance, 'field', 'institutionregex')) {
} $is_regex = (boolean) $configvalue->value;
else { }
foreach ($saml_attributes[$row->value] as $attr) { else {
if ($attr == $institution_value) { $is_regex = false;
$instance = $row; }
break; if ($configvalue = get_record('auth_instance_config', 'instance', $row->instance, 'field', 'institutionvalue')) {
} $institution_value = $configvalue->value;
}
else {
$institution_value = $row->institution;
}
if ($is_regex) {
foreach ($saml_attributes[$row->value] as $attr) {
if (preg_match('/'.trim($institution_value).'/', $attr)) {
$instance = $row;
break;
}