Added ability for authentication plugins to modify session keys (to allow invalidation when their own authentication data is changed) as well as the ability to disable the built-in password change facility
--- a/includes/sessions.php Sun Aug 09 01:27:45 2009 -0400
+++ b/includes/sessions.php Mon Aug 10 22:43:26 2009 -0400
@@ -172,6 +172,20 @@
var $csrf_token = false;
/**
+ * Password change disabled, for auth plugins
+ * @var bool
+ */
+
+ var $password_change_disabled = false;
+
+ /**
+ * Password change page URL + title, for auth plugins
+ * @var array
+ */
+
+ var $password_change_dest = array('url' => '', 'title' => '');
+
+ /**
* Switch to track if we're started or not.
* @access private
* @var bool
@@ -923,7 +937,16 @@
}
else
{
- $session_key = hmac_sha1($password_hmac, $salt);
+ $key_pieces = array($password_hmac);
+ $sk_mode = 'generate';
+ $code = $plugins->setHook('session_key_calc');
+ foreach ( $code as $cmd )
+ {
+ eval($cmd);
+ }
+ $key_pieces = implode("\xFF", $key_pieces);
+
+ $session_key = hmac_sha1($key_pieces, $salt);
}
// Minimum level
@@ -1246,7 +1269,16 @@
// $loose_call is turned on only from validate_aes_session
if ( !$loose_call )
{
- $correct_key = hexdecode(hmac_sha1($row['password'], $row['salt']));
+ $key_pieces = array($row['password']);
+ $user_id =& $row['uid'];
+ $sk_mode = 'validate';
+ $code = $plugins->setHook('session_key_calc');
+ foreach ( $code as $cmd )
+ {
+ eval($cmd);
+ }
+ $key_pieces = implode("\xFF", $key_pieces);
+ $correct_key = hexdecode(hmac_sha1($key_pieces, $row['salt']));
$user_key = hexdecode($key);
if ( $correct_key !== $user_key || !is_string($user_key) )
{
@@ -1530,8 +1562,41 @@
}
/**
- * Grabs the user's password MD5
- * @return string, or bool false if access denied
+ * Prevent the user from changing their password. Authentication plugins may call this to enforce single sign-on.
+ * @param string URL to page where the user may change their password
+ * @param string Title of the page where the user may change their password
+ * @return null
+ */
+
+ function disable_password_change($change_url = false, $change_title = false)
+ {
+ if ( $this->password_change_disabled )
+ {
+ // don't allow calling twice. if we have two plugins doing this, somebody is bad at configuring websites.
+ return false;
+ }
+
+ if ( is_string($change_url) && is_string($change_title) )
+ {
+ $this->password_change_dest = array(
+ 'url' => $change_url,
+ 'title' => $change_title
+ );
+ }
+ else
+ {
+ $this->password_change_dest = array(
+ 'url' => false,
+ 'title' => false
+ );
+ }
+
+ $this->password_change_disabled = true;
+ }
+
+ /**
+ * Grabs the user's password MD5 - NOW DEPRECATED AND DISABLED.
+ * @return bool false
*/
function grab_password_hash()
@@ -2261,178 +2326,76 @@
}
/**
- * Updates a user's information in the database. Note that any of the values except $user_id can be false if you want to preserve the old values.
- * Not localized because this really isn't used a whole lot anymore.
+ * Change a user's e-mail address.
* @param int $user_id The user ID of the user to update - this cannot be changed
- * @param string $username The new username
- * @param string $old_pass The current password - only required if sessionManager::$user_level < USER_LEVEL_ADMIN. This should usually be an UNENCRYPTED string. This can also be an array - if it is, key 0 is treated as data AES-encrypted with key 1
- * @param string $password The new password
* @param string $email The new e-mail address
- * @param string $realname The new real name
- * @param string $signature The updated forum/comment signature
- * @param int $user_level The updated user level
* @return string 'success' if successful, or array of error strings on failure
*/
- function update_user($user_id, $username = false, $old_pass = false, $password = false, $email = false, $realname = false, $signature = false, $user_level = false)
+ function change_email($user_id, $email)
{
global $db, $session, $paths, $template, $plugins; // Common objects
// Create some arrays
- $errors = Array(); // Used to hold error strings
- $strs = Array(); // Sub-query statements
+ $errors = array(); // Used to hold error strings
// Scan the user ID for problems
- if(intval($user_id) < 1) $errors[] = 'SQL injection attempt';
-
- // Instanciate the AES encryption class
- $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE);
-
- // If all of our input vars are false, then we've effectively done our job so get out of here
- if($username === false && $password === false && $email === false && $realname === false && $signature === false && $user_level === false)
- {
- // echo 'debug: $session->update_user(): success (no changes requested)';
- return 'success';
- }
-
- // Initialize our authentication check
- $authed = false;
+ if ( intval($user_id) < 1 )
+ $errors[] = 'SQL injection attempt';
- // Verify the inputted password
- if(is_string($old_pass))
- {
- $q = $this->sql('SELECT password FROM '.table_prefix.'users WHERE user_id='.$user_id.';');
- if($db->numrows() < 1)
- {
- $errors[] = 'The password data could not be selected for verification.';
- }
- else
- {
- $row = $db->fetchrow();
- $real = $aes->decrypt($row['password'], $this->private_key, ENC_HEX);
- if($real == $old_pass)
- $authed = true;
- }
- }
-
- elseif(is_array($old_pass))
- {
- $old_pass = $aes->decrypt($old_pass[0], $old_pass[1]);
- $q = $this->sql('SELECT password FROM '.table_prefix.'users WHERE user_id='.$user_id.';');
- if($db->numrows() < 1)
- {
- $errors[] = 'The password data could not be selected for verification.';
- }
- else
- {
- $row = $db->fetchrow();
- $real = $aes->decrypt($row['password'], $this->private_key, ENC_HEX);
- if($real == $old_pass)
- $authed = true;
- }
- }
-
- // Initialize our query
- $q = 'UPDATE '.table_prefix.'users SET ';
+ $user_id = intval($user_id);
- if($this->auth_level >= USER_LEVEL_ADMIN || $authed) // Need the current password in order to update the e-mail address, change the username, or reset the password
- {
- // Username
- if(is_string($username))
- {
- // Check the username for problems
- if(!preg_match('#^'.$this->valid_username.'$#', $username))
- $errors[] = 'The username you entered contains invalid characters.';
- $strs[] = 'username=\''.$db->escape($username).'\'';
- }
- // Password
- if(is_string($password) && strlen($password) >= 6)
- {
- // Password needs to be encrypted before being stashed
- $encpass = $aes->encrypt($password, $this->private_key, ENC_HEX);
- if(!$encpass)
- $errors[] = 'The password could not be encrypted due to an internal error.';
- $strs[] = 'password=\''.$encpass.'\'';
- }
- // E-mail addy
- if(is_string($email))
- {
- if(!check_email_address($email))
- $errors[] = 'The e-mail address you entered is invalid.';
- $strs[] = 'email=\''.$db->escape($email).'\'';
- }
- }
- // Real name
- if(is_string($realname))
- {
- $strs[] = 'real_name=\''.$db->escape($realname).'\'';
- }
- // Forum/comment signature
- if(is_string($signature))
- {
- $strs[] = 'signature=\''.$db->escape($signature).'\'';
- }
- // User level
- if(is_int($user_level))
- {
- $strs[] = 'user_level='.$user_level;
- }
+ // Verify e-mail address
+ if ( !check_email_address($email) )
+ $errors[] = 'user_err_email_not_valid';
- // Add our generated query to the query string
- $q .= implode(',', $strs);
-
- // One last error check
- if(sizeof($strs) < 1) $errors[] = 'An internal error occured building the SQL query, this is a bug';
- if(sizeof($errors) > 0) return $errors;
+ if ( count($errors) > 0 )
+ return $errors;
- // Free our temp arrays
- unset($strs, $errors);
-
- // Finalize the query and run it
- $q .= ' WHERE user_id='.$user_id.';';
- $this->sql($q);
+ // Make query
+ $email = $db->escape($email);
+ $q = $db->sql_query('UPDATE ' . table_prefix . "users SET email = '$email' WHERE user_id = $user_id;");
// We also need to trigger re-activation.
- if ( is_string($email) )
+ switch(getConfig('account_activation', 'none'))
{
- switch(getConfig('account_activation'))
- {
- case 'user':
- case 'admin':
-
- if ( $session->user_level >= USER_LEVEL_MOD && getConfig('account_activation') == 'admin' )
- // Don't require re-activation by admins for admins
- break;
-
- // retrieve username
- if ( !$username )
+ case 'user':
+ case 'admin':
+
+ // Note: even with admin activation, activation e-mails are sent when an e-mail is changed.
+
+ if ( $session->user_level >= USER_LEVEL_MOD && getConfig('account_activation') == 'admin' )
+ // Trust admins and moderators
+ break;
+
+ // retrieve username
+ if ( !$username )
+ {
+ $q = $this->sql('SELECT username FROM ' . table_prefix . "users WHERE user_id = $user_id;");
+ if($db->numrows() < 1)
{
- $q = $this->sql('SELECT username FROM '.table_prefix.'users WHERE user_id='.$user_id.';');
- if($db->numrows() < 1)
- {
- $errors[] = 'The username could not be selected.';
- }
- else
- {
- $row = $db->fetchrow();
- $username = $row['username'];
- }
+ $errors[] = 'The username could not be selected.';
+ }
+ else
+ {
+ $row = $db->fetchrow();
+ $username = $row['username'];
}
- if ( !$username )
- return $errors;
-
- // Generate a totally random activation key
- $actkey = sha1 ( microtime() . mt_rand() );
- $a = $this->send_activation_mail($username, $actkey);
- if(!$a)
- {
- $this->admin_activation_request($username);
- }
- // Deactivate the account until e-mail is confirmed
- $q = $db->sql_query('UPDATE '.table_prefix.'users SET account_active=0,activation_key=\'' . $actkey . '\' WHERE user_id=' . $user_id . ';');
- break;
- }
+ }
+ if ( !$username )
+ return $errors;
+
+ // Generate an activation key
+ $actkey = sha1 ( microtime() . mt_rand() );
+ $a = $this->send_activation_mail($username, $actkey);
+ if(!$a)
+ {
+ $this->admin_activation_request($username);
+ }
+ // Deactivate the account until e-mail is confirmed
+ $q = $db->sql_query('UPDATE ' . table_prefix . "users SET account_active = 0, activation_key = '$actkey' WHERE user_id = $user_id;");
+ break;
}
// Yay! We're done
--- a/language/english/user.json Sun Aug 09 01:27:45 2009 -0400
+++ b/language/english/user.json Mon Aug 10 22:43:26 2009 -0400
@@ -96,6 +96,7 @@
err_locked_out: 'You have used up all %config.lockout_threshold% allowed login attempts. Please wait %time_rem% minute%plural% before attempting to log in again%captcha_blurb%.',
err_locked_out_captcha_blurb: ', or enter the visual confirmation code shown above in the appropriate box',
err_admin_session_timed_out: 'Your session has timed out; please log in again using the form above.',
+ err_email_not_valid: 'The e-mail address you entered is invalid.',
logout_success_title: 'Logged out',
logout_success_body: 'You have been successfully logged out, and all cookies have been cleared. You will now be transferred to the main page.',
@@ -282,11 +283,14 @@
emailpassword_err_demo: 'You can\'t change your password in demo mode.',
emailpassword_err_password_too_short: 'The new password must be 6 characters or greater in length.',
emailpassword_err_password_too_weak: 'Your password did not meet the complexity score requirement for this site. Your password scored %score%, while a score of at least %config.pw_strength_minimum% is needed.',
+ emailpassword_msg_change_disabled: 'You cannot change your password here because either a single sign-on is being used and your password is stored in a different location, or password authentication is disabled for this site.',
+ emailpassword_msg_change_disabled_url: 'To manage or change your login details, use the following link:',
emailpassword_msg_profile_success: 'Profile changed',
emailpassword_msg_pass_success: 'Password changed',
- emailpassword_msg_need_activ_user: 'Your password and e-mail address have been changed. Since e-mail activation is required on this site, you will need to re-activate your account to continue. An e-mail has been sent to the new e-mail address with an activation link. You must click that link in order to log in again.',
- emailpassword_msg_need_activ_admin: 'Your password and e-mail address have been changed. Since administrative activation is required on this site, a request has been sent to the administrators to activate your account for you. You will not be able to use your account until it is activated by an administrator.',
- emailpassword_msg_password_changed: 'Your password has been changed, and you will now be redirected back to the user control panel.',
+ emailpassword_msg_email_success: 'E-mail address changed',
+ emailpassword_msg_need_activ_user: 'Your profile has been changed. Since e-mail activation is required on this site, you will need to re-activate your account to continue. An e-mail has been sent to the new e-mail address with an activation link. You must click that link in order to log in again.',
+ emailpassword_msg_need_activ_admin: 'Your profile has been changed. Since account activation is required on this site, you will need to re-activate your account to continue. An e-mail has been sent to the new e-mail address with an activation link. You must click that link in order to log in again.',
+ emailpassword_msg_password_changed: 'Your profile has been changed successfully. You will now be redirected back to the user control panel.',
emailpassword_err_password_no_match: 'The passwords you entered do not match.',
emailpassword_grp_chpasswd: 'Change password',
emailpassword_field_newpass: 'Type a new password:',
--- a/plugins/SpecialUserPrefs.php Sun Aug 09 01:27:45 2009 -0400
+++ b/plugins/SpecialUserPrefs.php Mon Aug 10 22:43:26 2009 -0400
@@ -212,11 +212,10 @@
$db->_die();
$row = $db->fetchrow();
$db->free_result();
- $old_pass = $session->pk_decrypt($row['password'], ENC_HEX);
$new_email = $_POST['newemail'];
- $result = $session->update_user($session->user_id, false, $old_pass, false, $new_email);
+ $result = $session->change_email($session->user_id, $new_email);
if ( $result != 'success' )
{
$message = '<p>' . $lang->get('usercp_emailpassword_err_list') . '</p>';
@@ -226,9 +225,9 @@
$email_changed = true;
}
// Obtain password
- if ( !empty($_POST['crypt_data']) || !empty($_POST['newpass']) )
+ if ( !empty($_POST['crypt_data']) || !empty($_POST['newpass']) || $session->password_change_disabled )
{
- $newpass = $session->get_aes_post('newpass');
+ $newpass = $session->password_change_disabled ? '' : $session->get_aes_post('newpass');
// At this point we know if we _want_ to change the password...
// We can't check the password to see if it matches the confirmation
@@ -274,10 +273,31 @@
redirect(makeUrl(get_main_page()), $lang->get('usercp_emailpassword_msg_profile_success'), $lang->get('usercp_emailpassword_msg_need_activ_admin'), 20);
}
}
- $session->login_without_crypto($session->username, $newpass);
+ $session->login_without_crypto($username, $newpass);
redirect(makeUrlNS('Special', 'Preferences'), $lang->get('usercp_emailpassword_msg_pass_success'), $lang->get('usercp_emailpassword_msg_password_changed'), 5);
}
}
+ else if ( $email_changed )
+ {
+ $session->logout(USER_LEVEL_CHPREF);
+ $activation = $session->user_level >= USER_LEVEL_MOD ? 'none' : getConfig('account_activation', 'none');
+ switch($activation)
+ {
+ default:
+ $message_body = $lang->get('usercp_emailpassword_msg_password_changed');
+ $timeout = 5;
+ break;
+ case 'admin':
+ $message_body = $lang->get('usercp_emailpassword_msg_need_activ_user');
+ $timeout = 20;
+ break;
+ case 'user':
+ $message_body = $lang->get('usercp_emailpassword_msg_need_activ_admin');
+ $timeout = 20;
+ break;
+ }
+ redirect(makeUrlNS('Special', 'Preferences'), $lang->get('usercp_emailpassword_msg_email_success'), $message_body, $timeout);
+ }
}
}
$template->tpl_strings['PAGE_NAME'] = $lang->get('usercp_emailpassword_title');
@@ -308,20 +328,32 @@
}
echo '<form action="' . makeUrlNS('Special', 'Preferences/EmailPassword') . '" method="post" onsubmit="return runEncryption();" name="empwform" >';
+ echo '<fieldset>';
+ echo '<legend>' . $lang->get('usercp_emailpassword_grp_chpasswd') . '</legend>';
// Password change form
+ if ( $session->password_change_disabled )
+ {
+ echo '<p>' . $lang->get('usercp_emailpassword_msg_change_disabled') . '</p>';
+ if ( $session->password_change_dest['url'] )
+ {
+ echo '<p>' . $lang->get('usercp_emailpassword_msg_change_disabled_url') . '
+ <a onclick="window.open(this.href); return false;" href="' . htmlspecialchars($session->password_change_dest['url']) . '">' . htmlspecialchars($session->password_change_dest['title']) . '</a></p>';
+ }
+ }
+ else
+ {
+ echo $lang->get('usercp_emailpassword_field_newpass') . '<br />
+ <input type="password" name="newpass" size="30" tabindex="1" ' . ( getConfig('pw_strength_enable') == '1' ? 'onkeyup="password_score_field(this);" ' : '' ) . '/>' . ( getConfig('pw_strength_enable') == '1' ? '<span class="password-checker" style="font-weight: bold; color: #aaaaaa;"> Loading...</span>' : '' ) . '
+ <br />
+ <br />
+ ' . $lang->get('usercp_emailpassword_field_newpass_confirm') . '<br />
+ <input type="password" name="newpass_confirm" size="30" tabindex="2" />
+ ' . ( getConfig('pw_strength_enable') == '1' ? '<br /><br /><div id="pwmeter"></div>
+ <small>' . $lang->get('usercp_emailpassword_msg_password_min_score') . '</small>' : '' );
+ }
+ echo '</fieldset><br />';
echo '<fieldset>
- <legend>' . $lang->get('usercp_emailpassword_grp_chpasswd') . '</legend>
- ' . $lang->get('usercp_emailpassword_field_newpass') . '<br />
- <input type="password" name="newpass" size="30" tabindex="1" ' . ( getConfig('pw_strength_enable') == '1' ? 'onkeyup="password_score_field(this);" ' : '' ) . '/>' . ( getConfig('pw_strength_enable') == '1' ? '<span class="password-checker" style="font-weight: bold; color: #aaaaaa;"> Loading...</span>' : '' ) . '
- <br />
- <br />
- ' . $lang->get('usercp_emailpassword_field_newpass_confirm') . '<br />
- <input type="password" name="newpass_confirm" size="30" tabindex="2" />
- ' . ( getConfig('pw_strength_enable') == '1' ? '<br /><br /><div id="pwmeter"></div>
- <small>' . $lang->get('usercp_emailpassword_msg_password_min_score') . '</small>' : '' ) . '
- </fieldset><br />
- <fieldset>
<legend>' . $lang->get('usercp_emailpassword_grp_chemail') . '</legend>
' . $lang->get('usercp_emailpassword_field_newemail') . '<br />
<input type="text" value="' . ( isset($_POST['newemail']) ? htmlspecialchars($_POST['newemail']) : '' ) . '" name="newemail" size="30" tabindex="3" />
@@ -333,12 +365,14 @@
<br />
<div style="text-align: right;"><input type="submit" name="submit" value="' . $lang->get('etc_save_changes') . '" tabindex="5" /></div>';
- echo $session->generate_aes_form();
+ if ( !$session->password_change_disabled )
+ echo $session->generate_aes_form();
+
echo '</form>';
// ENCRYPTION CODE
?>
- <?php if ( getConfig('pw_strength_enable') == '1' ): ?>
+ <?php if ( !$session->password_change_disabled && getConfig('pw_strength_enable') == '1' ): ?>
<script type="text/javascript">
addOnloadHook(function()
{