Commit e1d5cd78 authored by Aaron Wells's avatar Aaron Wells Committed by Gerrit Code Review

Merge "Bug 1533377: Browserid end-of-life migration script"

parents 17f729ce cfef0ff9
......@@ -49,8 +49,12 @@ foreach (array_keys($plugins) as $plugin) {
'disableable' => call_static_method($classname, 'can_be_disabled'),
'deprecated' => call_static_method($classname, 'is_deprecated'),
'name' => call_static_method($classname, 'get_plugin_display_name'),
'enableable' => call_static_method($classname, 'is_usable')
);
if ($plugins[$plugin]['installed'][$key]['disableable'] || !$i->active) {
if (
($i->active && $plugins[$plugin]['installed'][$key]['disableable'])
|| (!$i->active && $plugins[$plugin]['installed'][$key]['enableable'])
){
$plugins[$plugin]['installed'][$key]['activateform'] = activate_plugin_form($plugin, $i);
}
if ($plugin == 'artefact') {
......
......@@ -29,6 +29,7 @@ $search = (object) array(
'loggedin' => param_alpha('loggedin', 'any'),
'loggedindate' => param_variable('loggedindate', strftime(get_string('strftimedatetimeshort'))),
'duplicateemail' => param_boolean('duplicateemail', false),
'authname' => param_alpha('authname', null),
);
$offset = param_integer('offset', 0);
......
......@@ -14,15 +14,14 @@ defined('INTERNAL') || die();
$string['browserid'] = 'Persona';
$string['title'] = 'Persona';
$string['description'] = 'Authenticate using Persona';
$string['notusable'] = 'Please install the PHP cURL extension and check the connection to the Persona verifier';
$string['notusable'] = 'Discontinued';
$string['badassertion'] = 'The given Persona assertion is not valid: %s';
$string['badverification'] = 'Mahara did not receive valid JSON output from the Persona verifier.';
$string['login'] = 'Persona';
$string['register'] = 'Register with Persona';
$string['missingassertion'] = 'Persona did not return an alpha-numeric assertion.';
$string['deprecatedmsg'] = "As of 30 November 2016, <a href=\"https://wiki.mozilla.org/Identity/Persona_Shutdown_Guidelines_for_Reliers\">Mozilla is discontinuing the Persona
authentication service</a>. This plugin is a placeholder to aid in migrating existing Persona accounts from Persona to internal authentication.";
$string['nobrowseridinstances'] = 'This site has no Persona auth instances, so no action needs to be taken.';
$string['emailalreadyclaimed'] = "Another user account has already claimed the email address '%s'.";
$string['emailclaimedasusername'] = "Another user account has already claimed the email address '%s' as a username.";
$string['browseridnotenabled'] = "The Persona authentication plugin is not enabled in any active institution.";
$string['emailnotfound'] = "A user account with an email address of '%s' was not found in any of the institutions where Persona is enabled.";
$string['institutioncolumn'] = 'Institution';
$string['numuserscolumn'] = 'Number of active Persona users';
$string['migratetitle'] = 'Auto-migrate Persona users';
$string['migratedesc'] = 'Automatically move all Persona users to internal auth, and delete all Persona auth instances.';
\ No newline at end of file
<!-- @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later -->
<!-- @copyright For copyright information on Mahara, please see the README file distributed with this software. -->
<h3>We auto-create users</h3>
<p>Users that successfully authenticate but are not users of this system yet
will have an account created automatically.</p>
<p>Their username will be their email address.</p>
<!-- @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later -->
<!-- @copyright For copyright information on Mahara, please see the README file distributed with this software. -->
<h3>Auto-migrate Persona users</h3>
<p>Mozilla is <a href="https://wiki.mozilla.org/Identity/Persona_Shutdown_Guidelines_for_Reliers">discontinuing the Persona authentication service</a> as of 30 November, 2016.
If your site is using Persona, you will need to migrate those users to a different authentication method. This plugin provides a basic option to change all Persona-based user
accounts on your site, to the Mahara "internal" authentication method.</p>
<p>Selecting "Yes" and saving this form, will activate the migration script, and do the following:</p>
<ol>
<li>All Persona auth instances on the site will be deleted.</li>
<li>All users who are on Persona authentication, will be switched to the Internal auth instance for their institution.</li>
<li>If their institution has no Internal auth instance, one will be created.</li>
<li>These users will <b>not</b> have a password set. They will need to use the "Forgot password" link (and their Persona email address) to set an initial Mahara password.</li>
<li>The users' usernames will be unchanged.</li>
</ol>
<p><b>Note:</b> Users will not receive any notification that their authentication method has changed. You may wish to put a message on your site's logged-out homepage to explain
to former Persona users that they should use the "Forgot password" link, and their Persona email address, to set a new password and access their Mahara account.</p>
......@@ -14,278 +14,162 @@ require_once(get_config('docroot') . 'auth/lib.php');
require_once(get_config('docroot') . 'lib/institution.php');
class AuthBrowserid extends Auth {
public function __construct($id = null) {
$this->has_instance_config = true;
$this->type = 'browserid';
$this->config['weautocreateusers'] = 0;
public function __construct($id = null) {
if (!empty($id)) {
return $this->init($id);
}
$this->ready = true;
return true;
}
public function init($id) {
$this->ready = parent::init($id);
return $this->ready;
}
public function authenticate_user_account($user, $password) {
// Authentication is done elsewhere in Javascript
return false;
}
public function can_auto_create_users() {
// The normal user auto creation process doesn't work for this backend
return false;
}
public function create_new_user($email) {
if (!$this->config['weautocreateusers']) {
return null;
}
if (record_exists('artefact_internal_profile_email', 'email', $email)) {
throw new AccountAutoCreationException(get_string('emailalreadyclaimed', 'auth.browserid', $email));
}
if (record_exists('usr', 'username', $email)) {
throw new AccountAutoCreationException(get_string('emailclaimedasusername', 'auth.browserid', $email));
}
// Personal details are currently not provided by the Persona API.
$user = new stdClass();
$user->username = $email;
$user->firstname = '';
$user->lastname = '';
$user->email = $email;
// no need for a password on Persona accounts
$user->password = '';
$user->passwordchange = 0;
$user->authinstance = $this->instanceid;
// Set default values to activate this user
$user->deleted = 0;
$user->expiry = null;
$user->suspendedcusr = null;
$user->id = create_user($user, array(), $this->institution);
return $user;
}
}
class PluginAuthBrowserid extends PluginAuth {
const BROWSERID_VERIFIER_URL = 'https://verifier.login.persona.org/verify';
private static $default_config = array(
'weautocreateusers' => 0,
);
public static function has_config() {
return false;
}
public static function get_config_options() {
return array();
}
public static function has_instance_config() {
return true;
}
/**
* Implement the function is_usable()
*
* @return boolean true if the BrowserID verifier is usable, false otherwise
*/
public static function is_usable() {
if ( extension_loaded('curl')) {
return self::is_available();
}
return false;
}
public static function get_instance_config_options($institution, $instance = 0) {
if ($instance > 0) {
$current_config = get_records_menu('auth_instance_config', 'instance', $instance, '', 'field, value');
if ($current_config == false) {
$current_config = array();
}
foreach (self::$default_config as $key => $value) {
if (array_key_exists($key, $current_config)) {
self::$default_config[$key] = $current_config[$key];
}
}
}
$elements = array(
'instance' => array(
'type' => 'hidden',
'value' => $instance,
),
'institution' => array(
'type' => 'hidden',
'value' => $institution,
),
'authname' => array(
'type' => 'hidden',
'value' => 'browserid',
),
'instancename' => array(
'type' => 'hidden',
'value' => 'Persona',
),
'authname' => array(
'type' => 'hidden',
'value' => 'browserid',
),
'weautocreateusers' => array(
'type' => 'switchbox',
'title' => get_string('weautocreateusers', 'auth'),
'defaultvalue' => self::$default_config['weautocreateusers'],
'help' => true
),
);
return array(
'elements' => $elements,
'renderer' => 'div'
public static function get_config_options() {
// Find out how many active users there are, with which instances,
// in which institutions.
$instances = get_records_sql_array(
'SELECT
i.displayname as displayname,
i.name as name,
(
SELECT COUNT(*)
FROM {usr} u
WHERE
u.authinstance = ai.id
AND deleted = 0
) AS numusers
FROM
{auth_instance} ai
INNER JOIN {institution} i
ON ai.institution = i.name
WHERE
ai.authname=\'browserid\'
ORDER BY
i.displayname
'
);
}
/**
* Function to check a BrowserID verifier status
* @return boolean true if the verifier is available, false otherwise
*/
public static function is_available(){
// Send a test assertion to the verification service
$request = array(
CURLOPT_URL => self::BROWSERID_VERIFIER_URL,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => 'request=1'
$elements = array();
$elements['helptext'] = array(
'type' => 'html',
'value' => get_string('deprecatedmsg', 'auth.browserid')
);
$response = mahara_http_request($request);
if (!empty($response->data)) {
$jsondata = json_decode($response->data);
return !empty($jsondata);
}
return false;
}
public static function save_instance_config_options($values, Pieform $form) {
$authinstance = new stdClass();
if ($values['instance'] > 0) {
$values['create'] = false;
$current = get_records_assoc('auth_instance_config', 'instance', $values['instance'], '', 'field, value');
$authinstance->id = $values['instance'];
if ($instances) {
$smarty = smarty_core();
$smarty->assign('instances', $instances);
$tablehtml = $smarty->fetch('auth:browserid:statustable.tpl');
$elements['statustable'] = array(
'type' => 'html',
'value' => $tablehtml
);
$elements['migrate'] = array(
'type' => 'switchbox',
'title' => get_string('migratetitle', 'auth.browserid'),
'description' => get_string('migratedesc', 'auth.browserid'),
'defaultvalue' => false,
'help' => true,
);
}
else {
$values['create'] = true;
$lastinstance = get_records_array('auth_instance', 'institution', $values['institution'], 'priority DESC', '*', '0', '1');
if ($lastinstance == false) {
$authinstance->priority = 0;
}
else {
$authinstance->priority = $lastinstance[0]->priority + 1;
}
$elements['noaction'] = array(
'type' => 'html',
'value' => get_string('nobrowseridinstances', 'auth.browserid')
);
}
$authinstance->institution = $values['institution'];
$authinstance->authname = $values['authname'];
$authinstance->instancename = $values['instancename'];
if ($values['create']) {
$values['instance'] = insert_record('auth_instance', $authinstance, 'id', true);
}
else {
update_record('auth_instance', $authinstance, array('id' => $values['instance']));
}
if (empty($current)) {
$current = array();
}
self::$default_config = array('weautocreateusers' => $values['weautocreateusers']);
$form = array(
'elements' => $elements
);
if ($instances) {
$form['elements']['js'] = array(
'type' => 'html',
'value' => <<<HTML
<script type="text/javascript">
if (typeof auth_browserid_reload_page === "undefined") {
var auth_browserid_reload_page = function() {
window.location.reload(true);
}
}
</script>
HTML
);
$form['jssuccesscallback'] = 'auth_browserid_reload_page';
}
return $form;
}
public static function save_config_options(Pieform $form, $values) {
if (!empty($values['migrate'])) {
$instances = get_records_array('auth_instance', 'authname', 'browserid', 'id');
foreach ($instances as $authinst) {
// Are there any users with this auth instance?
if (record_exists('usr', 'authinstance', $authinst->id)) {
// Find the internal auth instance for this institution
$internal = get_field('auth_instance', 'id', 'authname', 'internal', 'institution', $authinst->institution);
if (!$internal) {
// Institution has no internal auth instance. Create one.
$todb = new stdClass();
$todb->instancename = 'internal';
$todb->authname = 'internal';
$todb->institution = $authinst->institution;
$todb->priority = $authinst->priority;
$internal = insert_record('auth_instance', $todb, 'id', true);
}
foreach(self::$default_config as $field => $value) {
$record = new stdClass();
$record->instance = $values['instance'];
$record->field = $field;
$record->value = $value;
// Set the password & salt for Persona users to "*", which means "no password set"
update_record(
'usr',
(object)array(
'password' => '*',
'salt' => '*'
),
array(
'authinstance' => $authinst->id
)
);
set_field('usr', 'authinstance', $internal, 'authinstance', $authinst->id);
}
if ($values['create'] || !array_key_exists($field, $current)) {
insert_record('auth_instance_config', $record);
}
else {
update_record('auth_instance_config', $record, array('instance' => $values['instance'], 'field' => $field));
// Delete the Persona auth instance
delete_records('auth_remote_user', 'authinstance', $authinst->id);
delete_records('auth_instance_config', 'instance', $authinst->id);
delete_records('auth_instance', 'id', $authinst->id);
// Make it no longer be the parent authority to any auth instances
delete_records('auth_instance_config', 'field', 'parent', 'value', $authinst->id);
}
set_field('auth_installed', 'active', 0, 'name', 'browserid');
}
return $values;
}
/**
* Add a Persona link/button.
*/
public static function login_form_elements() {
return array(
'loginbrowserid' => array(
'value' => '<div class="login-externallink"><a class="persona-button btn btn-primary btn-xs" href="javascript:window.browserid_login()"><span>' . get_string('login', 'auth.browserid') . '</span></a></div>'
)
);
public static function has_instance_config() {
return false;
}
/**
* Load all of the Javascript needed to retrieve Personas from
* the browser.
* Implement the function is_usable()
*
* @return boolean true if the BrowserID verifier is usable, false otherwise
*/
public static function login_form_js() {
global $HEADDATA, $SESSION;
$HEADDATA[] = '<script src="https://login.persona.org/include.js" type="application/javascript"></script>';
$wwwroot = get_config('wwwroot');
$returnurl = hsc(get_relative_script_path());
// We can't use $USER->get('sesskey') because there is no $USER object yet.
$sesskey = get_random_key();
$SESSION->set('browseridsesskey', $sesskey);
return <<< EOF
<form id="browserid-form" action="{$wwwroot}auth/browserid/login.php" method="post">
<input id="browserid-assertion" type="hidden" name="assertion" value="">
<input id="browserid-returnurl" type="hidden" name="returnurl" value="{$returnurl}">
<input id="browserid-sesskey" type="hidden" name="sesskey" value="{$sesskey}">
<input style="display: none" type="submit">
</form>
<script type="application/javascript">
function browserid_login() {
navigator.id.get(function(assertion) {
if (assertion) {
document.getElementById('browserid-assertion').setAttribute('value', assertion);
document.getElementById('browserid-form').submit();
}
});
}
</script>
EOF;
}
public static function need_basic_login_form() {
public static function is_usable() {
return false;
}
public static function postinst($fromversion) {
// Deactivate for new installs or if not in use.
if ($fromversion == 0 || 0 == count_records('auth_instance', 'authname', 'browserid')) {
set_field('auth_installed', 'active', 0, 'name', 'browserid');
}
// Always deactivate this plugin, if it has been activated somehow.
set_field('auth_installed', 'active', 0, 'name', 'browserid');
}
public static function can_be_disabled() {
......@@ -296,122 +180,3 @@ EOF;
return true;
}
}
class BrowserIDUser extends LiveUser {
public function login($username, $password=null) {
// This will do one of 3 things
// 1 - If a user has an account, log them in
// 2 - If a user doesn't have an account, and there is an auth method (which also has weautocreate), create acc and login
// 3 - If a user doesn't have an account, and there is more than one auth method, show a registration page
$sql = "SELECT
a.id, i.name AS institutionname
FROM
{auth_instance} a
JOIN
{institution} i ON a.institution = i.name
WHERE
a.authname = 'browserid' AND
i.suspended = 0";
$authinstances = get_records_sql_array($sql, array());
if (!$authinstances) {
throw new ConfigException(get_string('browseridnotenabled', 'auth.browserid'));
}
$autocreate = array(); // Remember the authinstances that are happy to create users
foreach ($authinstances as $authinstance) {
$auth = AuthFactory::create($authinstance->id);
$institutionjoin = '';
$institutionwhere = '';
$sqlvalues = array($username);
if ($authinstance->institutionname != 'mahara') {
// Make sure that user is in the right institution
$institutionjoin = 'JOIN {usr_institution} ui ON ui.usr = u.id';
$institutionwhere = 'AND ui.institution = ?';
$sqlvalues[] = $authinstance->institutionname;
}
$sql = "SELECT
u.*,
" . db_format_tsfield('u.expiry', 'expiry') . ",
" . db_format_tsfield('u.lastlogin', 'lastlogin') . ",
" . db_format_tsfield('u.lastlastlogin', 'lastlastlogin') . ",
" . db_format_tsfield('u.lastaccess', 'lastaccess') . ",
" . db_format_tsfield('u.suspendedctime', 'suspendedctime') . ",
" . db_format_tsfield('u.ctime', 'ctime') . "
FROM
{usr} u
JOIN
{artefact_internal_profile_email} a ON a.owner = u.id
$institutionjoin
WHERE
a.verified = 1 AND
a.email = ?
$institutionwhere";
$user = get_record_sql($sql, $sqlvalues);
if (!$user) {
if ($auth->weautocreateusers) {
if ($authinstance->institutionname == 'mahara') {
array_unshift($autocreate, $auth); // Try "No Instititution" first when creating users below
}
else {
$autocreate[] = $auth;
}
}
continue; // skip to the next auth_instance
}
if (is_site_closed($user->admin)) {
return false;
}
ensure_user_account_is_active($user);
$this->authenticate($user, $auth->instanceid);
return true;
}
foreach ($autocreate as $auth) {
if (!$user = $auth->create_new_user($username)) {
continue;
}
$this->authenticate($user, $auth->instanceid);
return;
}
// Autocreation failed; try registration.
list($form, $registerconfirm) = auth_generate_registration_form('register', 'browserid', '/register.php');
if (!$form) {
throw new AuthUnknownUserException(get_string('emailnotfound', 'auth.browserid', $username));
}
if (record_exists('usr', 'email', $username)
|| record_exists('artefact_internal_profile_email', 'email', $username)) {
throw new AuthUnknownUserException(get_string('emailalreadytaken', 'auth.internal', $username));
}
$form['elements']['email'] = array(
'type' => 'hidden',
'value' => $username
);
$form['elements']['authtype'] = array(
'type' => 'hidden',
'value' => 'browserid'
);
list($formhtml, $js) = auth_generate_registration_form_js($form, $registerconfirm);
$registerdescription = get_string('registerwelcome');
if ($registerterms = get_config('registerterms')) {
$registerdescription .= ' ' . get_string('registeragreeterms');
}
$registerdescription .= ' ' . get_string('registerprivacy');
define('TITLE', get_string('register', 'auth.browserid'));
$smarty = smarty();
$smarty->assign('register_form', $formhtml);
$smarty->assign('registerdescription', $registerdescription);
if ($registerterms) {
$smarty->assign('termsandconditions', get_site_page_content('termsandconditions'));
}
$smarty->assign('INLINEJAVASCRIPT', $js);
$smarty->display('register.tpl');
die;
}
}
<?php
/**
*
* @package mahara
* @subpackage auth-browserid
* @author Francois Marier <francois@catalyst.net.nz>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
define('INTERNAL', 1);
define('PUBLIC', 1);
require('../../init.php');
safe_require('auth', 'browserid');
if (empty($_SESSION['browseridexpires']) || time() >= $_SESSION['browseridexpires']) {
$assertion = param_variable('assertion', null);
if (!$assertion) {
throw new AuthInstanceException(get_string('missingassertion','auth.browserid'));
}
// Send the assertion to the verification service
$request = array(
CURLOPT_URL => PluginAuthBrowserid::BROWSERID_VERIFIER_URL,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => 'assertion='.urlencode($assertion).'&audience='.get_audience(),
);
$response = mahara_http_request($request);
if (empty($response->data)) {
throw new AuthInstanceException(get_string('badverification','auth.browserid'));
}