# HG changeset patch # User Dan # Date 1249958606 14400 # Node ID fcc42560afe64f35fd0a3e900ae23ae34b577905 # Parent 67a4c839c7e1110b306aa57e3ee19d40f73d2eea 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 diff -r 67a4c839c7e1 -r fcc42560afe6 includes/sessions.php --- 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 diff -r 67a4c839c7e1 -r fcc42560afe6 language/english/user.json --- 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:', diff -r 67a4c839c7e1 -r fcc42560afe6 plugins/SpecialUserPrefs.php --- 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 = '

' . $lang->get('usercp_emailpassword_err_list') . '

'; @@ -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 '
'; + echo '
'; + echo '' . $lang->get('usercp_emailpassword_grp_chpasswd') . ''; // Password change form + if ( $session->password_change_disabled ) + { + echo '

' . $lang->get('usercp_emailpassword_msg_change_disabled') . '

'; + if ( $session->password_change_dest['url'] ) + { + echo '

' . $lang->get('usercp_emailpassword_msg_change_disabled_url') . ' + ' . htmlspecialchars($session->password_change_dest['title']) . '

'; + } + } + else + { + echo $lang->get('usercp_emailpassword_field_newpass') . '
+ ' . ( getConfig('pw_strength_enable') == '1' ? ' Loading...' : '' ) . ' +
+
+ ' . $lang->get('usercp_emailpassword_field_newpass_confirm') . '
+ + ' . ( getConfig('pw_strength_enable') == '1' ? '

+ ' . $lang->get('usercp_emailpassword_msg_password_min_score') . '' : '' ); + } + echo '

'; echo '
- ' . $lang->get('usercp_emailpassword_grp_chpasswd') . ' - ' . $lang->get('usercp_emailpassword_field_newpass') . '
- ' . ( getConfig('pw_strength_enable') == '1' ? ' Loading...' : '' ) . ' -
-
- ' . $lang->get('usercp_emailpassword_field_newpass_confirm') . '
- - ' . ( getConfig('pw_strength_enable') == '1' ? '

- ' . $lang->get('usercp_emailpassword_msg_password_min_score') . '' : '' ) . ' -

-
' . $lang->get('usercp_emailpassword_grp_chemail') . ' ' . $lang->get('usercp_emailpassword_field_newemail') . '
@@ -333,12 +365,14 @@
'; - echo $session->generate_aes_form(); + if ( !$session->password_change_disabled ) + echo $session->generate_aes_form(); + echo ''; // ENCRYPTION CODE ?> - + password_change_disabled && getConfig('pw_strength_enable') == '1' ): ?>