Skip to content
This repository has been archived by the owner on Apr 8, 2022. It is now read-only.

Commit

Permalink
Merge branch 'wip_MDL-47830_m29_pwrotation' of https://github.com/sko…
Browse files Browse the repository at this point in the history
  • Loading branch information
Sam Hemelryk authored and danpoltawski committed Dec 4, 2014
2 parents 5094463 + 1d65853 commit 6c4e70a
Show file tree
Hide file tree
Showing 15 changed files with 342 additions and 5 deletions.
5 changes: 5 additions & 0 deletions admin/settings/security.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
$temp->add(new admin_setting_configtext('minpasswordupper', new lang_string('minpasswordupper', 'admin'), new lang_string('configminpasswordupper', 'admin'), 1, PARAM_INT));
$temp->add(new admin_setting_configtext('minpasswordnonalphanum', new lang_string('minpasswordnonalphanum', 'admin'), new lang_string('configminpasswordnonalphanum', 'admin'), 1, PARAM_INT));
$temp->add(new admin_setting_configtext('maxconsecutiveidentchars', new lang_string('maxconsecutiveidentchars', 'admin'), new lang_string('configmaxconsecutiveidentchars', 'admin'), 0, PARAM_INT));

$temp->add(new admin_setting_configtext('passwordreuselimit',
new lang_string('passwordreuselimit', 'admin'),
new lang_string('passwordreuselimit_desc', 'admin'), 0, PARAM_INT));

$pwresetoptions = array(
300 => new lang_string('numminutes', '', 5),
900 => new lang_string('numminutes', '', 15),
Expand Down
3 changes: 3 additions & 0 deletions auth/email/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,16 @@ function user_signup($user, $notify=true) {
require_once($CFG->dirroot.'/user/profile/lib.php');
require_once($CFG->dirroot.'/user/lib.php');

$plainpassword = $user->password;
$user->password = hash_internal_user_password($user->password);
if (empty($user->calendartype)) {
$user->calendartype = $CFG->calendartype;
}

$user->id = user_create_user($user, false, false);

user_add_password_history($user->id, $plainpassword);

// Save any custom profile field information.
profile_save_data($user);

Expand Down
3 changes: 3 additions & 0 deletions auth/ldap/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ function user_signup($user, $notify=true) {
global $CFG, $DB, $PAGE, $OUTPUT;

require_once($CFG->dirroot.'/user/profile/lib.php');
require_once($CFG->dirroot.'/user/lib.php');

if ($this->user_exists($user->username)) {
print_error('auth_ldap_user_exists', 'auth_ldap');
Expand All @@ -553,6 +554,8 @@ function user_signup($user, $notify=true) {

$user->id = user_create_user($user, false, false);

user_add_password_history($user->id, $plainslashedpassword);

// Save any custom profile field information
profile_save_data($user);

Expand Down
2 changes: 2 additions & 0 deletions auth/upgrade.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ information provided here is intended especially for developers.

* Do not update user->firstaccess from any auth plugin, the complete_user_login() does it automatically.

* Add user_add_password_history() to user_signup() method.

=== 2.8 ===

* \core\session\manager::session_exists() now verifies the session is active
Expand Down
2 changes: 2 additions & 0 deletions lang/en/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,8 @@
$string['passwordchangelogout_desc'] = 'If enabled, when a password is changed, all browser sessions are terminated, apart from the one in which the new password is specified. (This setting does not affect password changes via bulk user upload.)';
$string['passwordpolicy'] = 'Password policy';
$string['passwordresettime'] = 'Maximum time to validate password reset request';
$string['passwordreuselimit'] = 'Password rotation limit';
$string['passwordreuselimit_desc'] = 'Number of times a user must change their password before they are allowed to reuse a password. Hashes of previously used passwords are stored in local database table. This feature might not be compatible with some external authentication plugins.';
$string['pathtoclam'] = 'clam AV path';
$string['pathtodot'] = 'Path to dot';
$string['pathtodot_help'] = 'Path to dot. Probably something like /usr/bin/dot. To be able to generate graphics from DOT files, you must have installed the dot executable and point to it here. Note that, for now, this only used by the profiling features (Development->Profiling) built into Moodle.';
Expand Down
2 changes: 2 additions & 0 deletions lang/en/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
$string['errorminpasswordlength'] = 'Passwords must be at least {$a} characters long.';
$string['errorminpasswordlower'] = 'Passwords must have at least {$a} lower case letter(s).';
$string['errorminpasswordnonalphanum'] = 'Passwords must have at least {$a} non-alphanumeric character(s).';
$string['errorpasswordreused'] = 'This password has been used before, and is not permitted to be reused';
$string['errorminpasswordupper'] = 'Passwords must have at least {$a} upper case letter(s).';
$string['errorpasswordupdate'] = 'Error updating password, password not changed';
$string['eventuserloggedin'] = 'User has logged in';
Expand All @@ -102,6 +103,7 @@
$string['informminpasswordlength'] = 'at least {$a} characters';
$string['informminpasswordlower'] = 'at least {$a} lower case letter(s)';
$string['informminpasswordnonalphanum'] = 'at least {$a} non-alphanumeric character(s)';
$string['informminpasswordreuselimit'] = 'Passwords can be reused after {$a} changes';
$string['informminpasswordupper'] = 'at least {$a} upper case letter(s)';
$string['informpasswordpolicy'] = 'The password must have {$a}';
$string['instructions'] = 'Instructions';
Expand Down
14 changes: 13 additions & 1 deletion lib/db/install.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20141201" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20141202" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
Expand Down Expand Up @@ -765,6 +765,18 @@
<INDEX NAME="courseid" UNIQUE="false" FIELDS="courseid"/>
</INDEXES>
</TABLE>
<TABLE NAME="user_password_history" COMMENT="A rotating log of hashes of previously used passwords for each user.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="hash" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="scale" COMMENT="Defines grading scales">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
Expand Down
26 changes: 26 additions & 0 deletions lib/db/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -4058,6 +4058,7 @@ function xmldb_main_upgrade($oldversion) {
// Moodle v2.8.0 release upgrade line.
// Put any upgrade step following this.


if ($oldversion < 2014120100.00) {

// Define field sslverification to be added to mnet_host.
Expand Down Expand Up @@ -4087,5 +4088,30 @@ function xmldb_main_upgrade($oldversion) {
// Main savepoint reached.
upgrade_main_savepoint(true, 2014120101.00);
}

if ($oldversion < 2014120102.00) {

// Define table user_password_history to be created.
$table = new xmldb_table('user_password_history');

// Adding fields to table user_password_history.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('hash', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);

// Adding keys to table user_password_history.
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));

// Conditionally launch create table for user_password_history.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}

// Main savepoint reached.
upgrade_main_savepoint(true, 2014120102.00);
}

return true;
}
3 changes: 3 additions & 0 deletions lib/moodlelib.php
Original file line number Diff line number Diff line change
Expand Up @@ -4283,6 +4283,9 @@ function delete_user(stdClass $user) {
// Purge user extra profile info.
$DB->delete_records('user_info_data', array('userid' => $user->id));

// Purge log of previous password hashes.
$DB->delete_records('user_password_history', array('userid' => $user->id));

// Last course access not necessary either.
$DB->delete_records('user_lastaccess', array('userid' => $user->id));
// Remove all user tokens.
Expand Down
3 changes: 3 additions & 0 deletions login/change_password.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
*/

require('../config.php');
require_once($CFG->dirroot.'/user/lib.php');
require_once('change_password_form.php');
require_once($CFG->libdir.'/authlib.php');

Expand Down Expand Up @@ -115,6 +116,8 @@
print_error('errorpasswordupdate', 'auth');
}

user_add_password_history($USER->id, $data->newpassword1);

if (!empty($CFG->passwordchangelogout)) {
\core\session\manager::kill_user_sessions($USER->id, session_id());
}
Expand Down
16 changes: 14 additions & 2 deletions login/change_password_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,15 @@ function definition() {
// visible elements
$mform->addElement('static', 'username', get_string('username'), $USER->username);

if (!empty($CFG->passwordpolicy)){
$mform->addElement('static', 'passwordpolicyinfo', '', print_password_policy());
$policies = array();
if (!empty($CFG->passwordpolicy)) {
$policies[] = print_password_policy();
}
if (!empty($CFG->passwordreuselimit) and $CFG->passwordreuselimit > 0) {
$policies[] = get_string('informminpasswordreuselimit', 'auth', $CFG->passwordreuselimit);
}
if ($policies) {
$mform->addElement('static', 'passwordpolicyinfo', '', implode('<br />', $policies));
}
$mform->addElement('password', 'password', get_string('oldpassword'));
$mform->addRule('password', get_string('required'), 'required', null, 'client');
Expand Down Expand Up @@ -92,6 +99,11 @@ function validation($data, $files) {
return $errors;
}

if (user_is_previously_used_password($USER->id, $data['newpassword1'])) {
$errors['newpassword1'] = get_string('errorpasswordreused', 'core_auth');
$errors['newpassword2'] = get_string('errorpasswordreused', 'core_auth');
}

$errmsg = '';//prevents eclipse warnings
if (!check_password_policy($data['newpassword1'], $errmsg)) {
$errors['newpassword1'] = $errmsg;
Expand Down
3 changes: 3 additions & 0 deletions login/lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ function core_login_process_password_reset_request() {
*/
function core_login_process_password_set($token) {
global $DB, $CFG, $OUTPUT, $PAGE, $SESSION;
require_once($CFG->dirroot.'/user/lib.php');

$pwresettime = isset($CFG->pwresettime) ? $CFG->pwresettime : 1800;
$sql = "SELECT u.*, upr.token, upr.timerequested, upr.id as tokenid
FROM {user} u
Expand Down Expand Up @@ -239,6 +241,7 @@ function core_login_process_password_set($token) {
if (!$userauth->user_update_password($user, $data->password)) {
print_error('errorpasswordupdate', 'auth');
}
user_add_password_history($user->id, $data->password);
if (!empty($CFG->passwordchangelogout)) {
\core\session\manager::kill_user_sessions($user->id, session_id());
}
Expand Down
15 changes: 14 additions & 1 deletion login/set_password_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
defined('MOODLE_INTERNAL') || die();

require_once($CFG->libdir.'/formslib.php');
require_once($CFG->dirroot.'/user/lib.php');

/**
* Set forgotten password form definition.
Expand Down Expand Up @@ -64,8 +65,15 @@ public function definition() {
// Visible elements.
$mform->addElement('static', 'username2', get_string('username'));

$policies = array();
if (!empty($CFG->passwordpolicy)) {
$mform->addElement('static', 'passwordpolicyinfo', '', print_password_policy());
$policies[] = print_password_policy();
}
if (!empty($CFG->passwordreuselimit) and $CFG->passwordreuselimit > 0) {
$policies[] = get_string('informminpasswordreuselimit', 'auth', $CFG->passwordreuselimit);
}
if ($policies) {
$mform->addElement('static', 'passwordpolicyinfo', '', implode('<br />', $policies));
}
$mform->addElement('password', 'password', get_string('newpassword'), $autocomplete);
$mform->addRule('password', get_string('required'), 'required', null, 'client');
Expand Down Expand Up @@ -103,6 +111,11 @@ public function validation($data, $files) {
return $errors;
}

if (user_is_previously_used_password($USER->id, $data['password'])) {
$errors['password'] = get_string('errorpasswordreused', 'core_auth');
$errors['password2'] = get_string('passwordreused', 'core_auth');
}

return $errors;
}
}
80 changes: 79 additions & 1 deletion user/lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -878,4 +878,82 @@ function user_get_user_navigation_info($user, $page) {
}

return $returnobject;
}
}

/**
* Add password to the list of used hashes for this user.
*
* This is supposed to be used from:
* 1/ change own password form
* 2/ password reset process
* 3/ user signup in auth plugins if password changing supported
*
* @param int $userid user id
* @param string $password plaintext password
* @return void
*/
function user_add_password_history($userid, $password) {
global $CFG, $DB;
require_once($CFG->libdir.'/password_compat/lib/password.php');

if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
return;
}

// Note: this is using separate code form normal password hashing because
// we need to have this under control in the future. Also the auth
// plugin might not store the passwords locally at all.

$record = new stdClass();
$record->userid = $userid;
$record->hash = password_hash($password, PASSWORD_DEFAULT);
$record->timecreated = time();
$DB->insert_record('user_password_history', $record);

$i = 0;
$records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
foreach ($records as $record) {
$i++;
if ($i > $CFG->passwordreuselimit) {
$DB->delete_records('user_password_history', array('id' => $record->id));
}
}
}

/**
* Was this password used before on change or reset password page?
*
* The $CFG->passwordreuselimit setting determines
* how many times different password needs to be used
* before allowing previously used password again.
*
* @param int $userid user id
* @param string $password plaintext password
* @return bool true if password reused
*/
function user_is_previously_used_password($userid, $password) {
global $CFG, $DB;
require_once($CFG->libdir.'/password_compat/lib/password.php');

if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
return false;
}

$reused = false;

$i = 0;
$records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
foreach ($records as $record) {
$i++;
if ($i > $CFG->passwordreuselimit) {
$DB->delete_records('user_password_history', array('id' => $record->id));
continue;
}
// NOTE: this is slow but we cannot compare the hashes directly any more.
if (password_verify($password, $record->hash)) {
$reused = true;
}
}

return $reused;
}
Loading

0 comments on commit 6c4e70a

Please sign in to comment.