Added possibility for auth plugins, which can log a user in using non-standard authentication methods.
authorDan
Thu, 26 Feb 2009 01:07:32 -0500
changeset 843 4415e50e4e84
parent 842 f13bb4f21890
child 844 7549f2880c32
Added possibility for auth plugins, which can log a user in using non-standard authentication methods.
includes/clientside/static/crypto.js
includes/clientside/static/login.js
includes/paths.php
includes/sessions.php
includes/template.php
plugins/SpecialUserFuncs.php
--- a/includes/clientside/static/crypto.js	Thu Feb 26 01:06:58 2009 -0500
+++ b/includes/clientside/static/crypto.js	Thu Feb 26 01:07:32 2009 -0500
@@ -1981,7 +1981,6 @@
       var hexch = text.substr(i, 3);
       if ( hexch.match(/^%[a-f0-9][a-f0-9]$/i) )
       {
-        console.debug('hexch: ', hexch);
         result[result.length] = (unescape(hexch)).charCodeAt(0);
         a = true;
         i += 2;
--- a/includes/clientside/static/login.js	Thu Feb 26 01:06:58 2009 -0500
+++ b/includes/clientside/static/login.js	Thu Feb 26 01:07:32 2009 -0500
@@ -347,6 +347,10 @@
       
       break;
       
+    default:
+      eval(setHook('login_set_status'));
+      break;
+      
     case AJAX_STATUS_DESTROY:
     case null:
     case undefined:
@@ -556,6 +560,8 @@
   tr2.appendChild(td2_2);
   table.appendChild(tr2);
   
+  eval(setHook('login_build_form'));
+  
   // Field - captcha
   if ( show_captcha )
   {
@@ -806,11 +812,11 @@
     }
   }
   
-  if ( !username )
+  if ( typeof(username) != 'string' )
   {
     var username = document.getElementById('ajax_login_field_username').value;
   }
-  if ( !password )
+  if ( typeof(password) != 'string' )
   {
     var password = document.getElementById('ajax_login_field_password').value;
   }
@@ -851,10 +857,14 @@
   ajaxLoginSetStatus(AJAX_STATUS_LOGGING_IN);
   
   // Encrypt the password and username
-  var userinfo = toJSONString({
+  var userinfo = {
       username: username,
       password: password
-    });
+    };
+    
+  eval(setHook('login_build_userinfo'));
+    
+  userinfo = toJSONString(userinfo);
   var crypt_key_ba = hexToByteArray(crypt_key);
   userinfo = stringToByteArray(userinfo);
   
@@ -957,10 +967,18 @@
 
 window.ajaxLoginGetErrorText = function(response)
 {
+  if ( !response.error_code.match(/^[a-z0-9]+_[a-z0-9_]+$/) )
+  {
+    return response.error_code;
+  }
   switch ( response.error_code )
   {
     default:
-      return $lang.get('user_err_' + response.error_code);
+      var ls = $lang.get('user_err_' + response.error_code);
+      if ( ls == 'user_err_' + response.error_code )
+        ls = $lang.get(response.error_code);
+      
+      return ls;
       break;
     case 'locked_out':
       if ( response.respawn_info.lockout_info.lockout_policy == 'lockout' )
@@ -1005,13 +1023,18 @@
         switch ( response.respawn_info.lockout_info.lockout_policy )
         {
           case 'captcha':
-            base += $lang.get('user_err_invalid_credentials_lockout', { 
+            base += $lang.get('user_err_invalid_credentials_lockout_captcha', { 
                 fails: response.respawn_info.lockout_info.lockout_fails,
                 lockout_threshold: response.respawn_info.lockout_info.lockout_threshold,
                 lockout_duration: response.respawn_info.lockout_info.lockout_duration
               });
             break;
           case 'lockout':
+            base += $lang.get('user_err_invalid_credentials_lockout', { 
+                fails: response.respawn_info.lockout_info.lockout_fails,
+                lockout_threshold: response.respawn_info.lockout_info.lockout_threshold,
+                lockout_duration: response.respawn_info.lockout_info.lockout_duration
+              });
             break;
         }
       }
--- a/includes/paths.php	Thu Feb 26 01:06:58 2009 -0500
+++ b/includes/paths.php	Thu Feb 26 01:07:32 2009 -0500
@@ -476,12 +476,12 @@
     foreach($k as $key)
     {
       $i++;
-      $name = ( preg_match('/^[a-z0-9_]+$/', $key) ) ? $lang->get($key) : $key;
+      $name = $lang->get($key);
       $ret .= "['".$name."', 'javascript:trees[0].toggle($i)', \n";
       foreach($this->admin_tree[$key] as $c)
       {
         $i++;
-        $name = ( preg_match('/^[a-z0-9_]+$/', $key) ) ? $lang->get($c['name']) : $c['name'];
+        $name = $lang->get($c['name']);
         if ( $c['icon'] && $c['icon'] != cdnPath . '/images/spacer.gif' )
         {
           if ( is_array($c['icon']) )
--- a/includes/sessions.php	Thu Feb 26 01:06:58 2009 -0500
+++ b/includes/sessions.php	Thu Feb 26 01:07:32 2009 -0500
@@ -658,20 +658,10 @@
     
     if ( !defined('IN_ENANO_INSTALL') )
     {
-      // Lockout stuff
-      $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5;
-      $duration  = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15;
-      // convert to minutes
-      $duration  = $duration * 60;
-      $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout';
-      
-      $timestamp_cutoff = time() - $duration;
-      $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
-      $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;');
-      $fails = $db->numrows();
+      $locked_out = $this->get_lockout_info($lockout_data);
       
       $captcha_good = false;
-      if ( $policy == 'captcha' && $captcha_hash && $captcha_code )
+      if ( $lockout_data['lockout_policy'] == 'captcha' && $captcha_hash && $captcha_code )
       {
         // policy is captcha -- check if it's correct, and if so, bypass lockout check
         $real_code = $this->get_captcha($captcha_hash);
@@ -682,7 +672,7 @@
       }
       if ( $policy != 'disable' && !$captcha_good )
       {
-        if ( $fails >= $threshold )
+        if ( $lockout_data['lockout_fails'] >= $lockout_data['lockout_threshold'] )
         {
           // ooh boy, somebody's in trouble ;-)
           $row = $db->fetchrow();
@@ -690,12 +680,12 @@
           return array(
               'success' => false,
               'error' => 'locked_out',
-              'lockout_threshold' => $threshold,
-              'lockout_duration' => ( $duration / 60 ),
-              'lockout_fails' => $fails,
-              'lockout_policy' => $policy,
-              'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ),
-              'lockout_last_time' => $row['timestamp']
+              'lockout_threshold' => $lockout_data['lockout_threshold'],
+              'lockout_duration' => ( $lockout_data['lockout_duration'] ),
+              'lockout_fails' => $lockout_data['lockout_fails'],
+              'lockout_policy' => $lockout_data['lockout_policy'],
+              'time_rem' => $lockout_data['lockout_time_rem'],
+              'lockout_last_time' => $lockout_data['lockout_last_time']
             );
         }
       }
@@ -729,19 +719,19 @@
                       . '\''.$db->escape($_SERVER['REMOTE_ADDR']).'\')');
       
       // Do we also need to increment the lockout countdown?
-      if ( @$policy != 'disable' && !defined('IN_ENANO_INSTALL') )
+      if ( @$lockout_data['lockout_policy'] != 'disable' && !defined('IN_ENANO_INSTALL') )
       {
         $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
         // increment fail count
         $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');');
-        $fails++;
+        $lockout_data['lockout_fails']++;
         return array(
             'success' => false,
-            'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials',
-            'lockout_threshold' => $threshold,
-            'lockout_duration' => ( $duration / 60 ),
-            'lockout_fails' => $fails,
-            'lockout_policy' => $policy
+            'error' => ( $lockout_data['lockout_fails'] >= $lockout_data['lockout_threshold'] ) ? 'locked_out' : 'invalid_credentials',
+            'lockout_threshold' => $lockout_data['lockout_threshold'],
+            'lockout_duration' => ( $lockout_data['lockout_duration'] ),
+            'lockout_fails' => $lockout_data['lockout_fails'],
+            'lockout_policy' => $lockout_data['lockout_policy']
           );
       }
       
@@ -851,19 +841,19 @@
         $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.enano_date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')');
         
       // Do we also need to increment the lockout countdown?
-      if ( !defined('IN_ENANO_INSTALL') && $policy != 'disable' )
+      if ( !defined('IN_ENANO_INSTALL') && $lockout_data['lockout_policy'] != 'disable' )
       {
         $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
         // increment fail count
         $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');');
-        $fails++;
+        $lockout_data['lockout_fails']++;
         return array(
             'success' => false,
-            'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials',
-            'lockout_threshold' => $threshold,
-            'lockout_duration' => ( $duration / 60 ),
-            'lockout_fails' => $fails,
-            'lockout_policy' => $policy
+            'error' => ( $lockout_data['lockout_fails'] >= $lockout_data['lockout_threshold'] ) ? 'locked_out' : 'invalid_credentials',
+            'lockout_threshold' => $lockout_data['lockout_threshold'],
+            'lockout_duration' => ( $lockout_data['lockout_duration'] ),
+            'lockout_fails' => $lockout_data['lockout_fails'],
+            'lockout_policy' => $lockout_data['lockout_policy']
           );
       }
         
@@ -1009,6 +999,49 @@
   }
   
   /**
+   * Tells us if we're locked out from logging in or not.
+   * @param reference will be filled with information regarding in-progress lockout
+   * @return bool True if locked out, false otherwise
+   */
+  
+  function get_lockout_info(&$lockdata)
+  {
+    global $db;
+    
+    // this has to be initialized to hide warnings
+    $lockdata = null;
+    
+    // Query database for lockout info
+    $locked_out = false;
+    $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5;
+    $duration  = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15;
+    // convert to minutes
+    $duration  = $duration * 60;
+    $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout';
+    if ( $policy != 'disable' )
+    {
+      $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+      $timestamp_cutoff = time() - $duration;
+      $q = $this->sql('SELECT timestamp FROM ' . table_prefix . 'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;');
+      $fails = $db->numrows();
+      $row = $db->fetchrow();
+      $locked_out = ( $fails >= $threshold );
+      $lockdata = array(
+          'locked_out' => $locked_out,
+          'lockout_threshold' => $threshold,
+          'lockout_duration' => ( $duration / 60 ),
+          'lockout_fails' => $fails,
+          'lockout_policy' => $policy,
+          'lockout_last_time' => $row['timestamp'],
+          'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ),
+          'captcha' => ''
+        );
+      $db->free_result();
+    }
+    return $locked_out;
+  }
+  
+  /**
    * Creates/restores a guest session
    * @todo implement real session management for guests
    */
@@ -1038,7 +1071,7 @@
       @setlocale(LC_ALL, $lang->lang_code);
     }
     // make a CSRF token
-    $this->csrf_token = sha1($_SERVER['REMOTE_ADDR'] . sha1($this->private_key));
+    $this->csrf_token = hmac_sha1($_SERVER['REMOTE_ADDR'], sha1($this->private_key));
   }
   
   /**
@@ -3821,34 +3854,7 @@
         
         $this->start();
         
-        // Query database for lockout info
-        $locked_out = false;
-        // are we locked out?
-        $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5;
-        $duration  = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15;
-        // convert to minutes
-        $duration  = $duration * 60;
-        $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout';
-        if ( $policy != 'disable' )
-        {
-          $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
-          $timestamp_cutoff = time() - $duration;
-          $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;');
-          $fails = $db->numrows();
-          $row = $db->fetchrow();
-          $locked_out = ( $fails >= $threshold );
-          $lockdata = array(
-              'locked_out' => $locked_out,
-              'lockout_threshold' => $threshold,
-              'lockout_duration' => ( $duration / 60 ),
-              'lockout_fails' => $fails,
-              'lockout_policy' => $policy,
-              'lockout_last_time' => $row['timestamp'],
-              'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ),
-              'captcha' => ''
-            );
-          $db->free_result();
-        }
+        $locked_out = $this->get_lockout_info($lockdata);
         
         $response = array('mode' => 'build_box');
         $response['allow_diffiehellman'] = $dh_supported;
@@ -3862,7 +3868,7 @@
         $response['locked_out'] = $locked_out;
         
         $response['lockout_info'] = $lockdata;
-        if ( $policy == 'captcha' && $locked_out )
+        if ( $lockdata['lockout_policy'] == 'captcha' && $locked_out )
         {
           $response['lockout_info']['captcha'] = $this->make_captcha();
         }
@@ -3992,6 +3998,41 @@
         $username =& $userinfo['username'];
         $password =& $userinfo['password'];
         
+        // At this point if any extra info was injected into the login data packet, we need to let plugins process it
+        /**
+         * Called upon processing an incoming login request. If you added anything to the userinfo object during the jshook
+         * login_build_userinfo, that will be in the $userinfo array here. Expected return values are: true if your plugin has
+         * not only succeeded but ALSO issued a session key (bypass the whole Enano builtin login process) and an associative array
+         * with "mode" set to "error" and an error string in "error" to send an error back to the client. Any return value other
+         * than these will be ignored.
+         * @hook login_process_userdata_json
+         */
+        
+        $code = $plugins->setHook('login_process_userdata_json');
+        foreach ( $code as $cmd )
+        {
+          $result = eval($cmd);
+          if ( $result === true )
+          {
+            return array(
+                'mode' => 'login_success',
+                'key' => ( $this->sid_super ) ? $this->sid_super : false
+              );
+          }
+          else if ( is_array($result) )
+          {
+            if ( isset($result['mode']) && $result['mode'] === 'error' && isset($result['error']) )
+            {
+              return array(
+                'mode' => 'login_failure',
+                'error_code' => $result['error'],
+                // Use this to provide a way to respawn the login box
+                'respawn_info' => $this->process_login_request(array('mode' => 'getkey'))
+              );
+            }
+          }
+        }
+        
         // If we're logging in with a temp password, attach to the login_password_reset hook to send our JSON response
         // A bit hackish since it just dies with the response :-(
         $plugins->attachHook('login_password_reset', '$this->process_login_request(array(\'mode\' => \'respond_password_reset\', \'user_id\' => $row[\'user_id\'], \'temp_password\' => $this->pk_encrypt($password)));');
--- a/includes/template.php	Thu Feb 26 01:06:58 2009 -0500
+++ b/includes/template.php	Thu Feb 26 01:07:32 2009 -0500
@@ -1499,7 +1499,7 @@
     }
     
     // Full pathname of template file
-    $tpl_file_fullpath = ENANO_ROOT . '/themes/' . $this->theme . '/' . $file;
+    $tpl_file_fullpath = ( strstr($file, '/') ) ? $file : ENANO_ROOT . '/themes/' . $this->theme . '/' . $file;
     
     // Make sure the template even exists
     if ( !is_file($tpl_file_fullpath) )
--- a/plugins/SpecialUserFuncs.php	Thu Feb 26 01:06:58 2009 -0500
+++ b/plugins/SpecialUserFuncs.php	Thu Feb 26 01:07:32 2009 -0500
@@ -264,6 +264,9 @@
         $errstring = $lang->get('user_err_locked_out', array('plural' => $s, 'captcha_blurb' => $captcha_string, 'time_rem' => $time_rem));
         
         break;
+      default:
+        $errstring = $lang->get($errstring);
+        break;
     }
     echo '<div class="error-box-mini">'.$errstring.'</div>';
   }
@@ -343,6 +346,11 @@
          }
          ?>
          <?php
+         $code = $plugins->setHook('login_form_html');
+         foreach ( $code as $cmd )
+         {
+           eval($cmd);
+         }
          if ( $level <= USER_LEVEL_MEMBER )
          {
            // "remember me" switch
@@ -504,7 +512,53 @@
       return false;
     }
     
-    $result = $session->login_without_crypto($_POST['username'], $password, false, intval($_POST['auth_level']), $captcha_hash, $captcha_code, isset($_POST['remember']));
+    // These are to allow auth plugins to work universally between JSON and HTML login forms
+    $userinfo =& $_POST;
+    $req = array(
+      'level' => intval($_POST['auth_level']),
+      'remember' => isset($_POST['remember'])
+    );
+    
+    // At this point if any extra fields were injected into the login form, we need to let plugins process it
+    
+    /**
+     * Called upon processing an incoming login request from the plain HTML login form.. If you added anything to the form,
+     * that will be in the $userinfo array here and on $_POST. Expected return values are: true if your plugin has
+     * not only succeeded but ALSO issued a session key (bypass the whole Enano builtin login process) and an associative array
+     * with "mode" set to "error" and an error string in "error" to send an error back to the client. Any return value other
+     * than these will be ignored.
+     * @hook login_process_userdata_json
+     */
+     
+    $skip_normal_login = false;
+    
+    $code = $plugins->setHook('login_process_userdata_json');
+    foreach ( $code as $cmd )
+    {
+      $result = eval($cmd);
+      if ( $result === true )
+      {
+        $skip_normal_login = true;
+        $result = array('success' => true);
+        break;
+      }
+      else if ( is_array($result) )
+      {
+        if ( isset($result['mode']) && $result['mode'] === 'error' && isset($result['error']) )
+        {
+          $__login_status = array(
+            'mode' => 'error',
+            'error' => $result['error']
+          );
+          return false;
+        }
+      }
+    }
+    
+    if ( !$skip_normal_login )
+    {
+      $result = $session->login_without_crypto($_POST['username'], $password, false, intval($_POST['auth_level']), $captcha_hash, $captcha_code, isset($_POST['remember']));
+    }
    
     if($result['success'])
     {