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
authorDan
Mon, 10 Aug 2009 22:43:26 -0400
changeset 1079 fcc42560afe6
parent 1078 67a4c839c7e1
child 1080 6358f769ecb1
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
includes/sessions.php
language/english/user.json
plugins/SpecialUserPrefs.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
--- 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()
         {