source: trunk/lib/Auth_SQL.inc.php @ 174

Last change on this file since 174 was 174, checked in by scdev, 18 years ago

Q - added move method to ACL class, added polish.

File size: 42.4 KB
Line 
1<?php
2/**
3 * The Auth_SQL class provides a SQL implementation for authentication.
4 *
5 * @author  Quinn Comendant <quinn@strangecode.com>
6 * @version 2.1
7 */
8
9// Available encryption types for class Auth_SQL.
10define('AUTH_ENCRYPT_PLAINTEXT', 1);
11define('AUTH_ENCRYPT_CRYPT', 2);
12define('AUTH_ENCRYPT_SHA1', 3);
13define('AUTH_ENCRYPT_SHA1_HARDENED', 4);
14define('AUTH_ENCRYPT_MD5', 5);
15define('AUTH_ENCRYPT_MD5_HARDENED', 6);
16
17require_once dirname(__FILE__) . '/Email.inc.php';
18
19class Auth_SQL {
20       
21    // Namespace of this auth object.
22    var $_ns;
23   
24    // Static var for test.
25    var $_authentication_tested;
26
27    // Paramters to be configured by setParam.
28    var $_params = array();
29    var $_default_params = array(
30
31        // Automatically create table and verify columns. Better set to false after site launch.
32        'create_table' => true,
33
34        // The database table containing users to authenticate.
35        'db_table' => 'user_tbl',
36
37        // The name of the primary key for the db_table.
38        'db_primary_key' => 'user_id',
39
40        // The name of the username key for the db_table.
41        'db_username_column' => 'username',
42
43        // If using the db_login_table feature, specify the db_login_table. The primary key must match the primary key for the db_table.
44        'db_login_table' => 'user_login_tbl',
45
46        // The type of encryption to use for passwords stored in the db_table. Use one of the AUTH_ENCRYPT_* types specified above.
47        // Hardened password hashes rely on the same key/salt being used to compare encryptions.
48        // Be aware that when using one of the hardened types the App signing_key or $more_salt below cannot change!
49        'encryption_type' => AUTH_ENCRYPT_MD5,
50
51        // The URL to the login script.
52        'login_url' => '/',
53
54        // The maximum amount of time a user is allowed to be logged in. They will be forced to login again if they expire.
55        // In seconds. 21600 seconds = 6 hours.
56        'login_timeout' => 21600,
57
58        // The maximum amount of time a user is allowed to be idle before their session expires. They will be forced to login again if they expire.
59        // In seconds. 3600 seconds = 1 hour.
60        'idle_timeout' => 3600,
61
62        // The period of time to compare login abuse attempts. If a threshold of logins is reached in this amount of time the account is blocked.
63        // Days and hours, like this: 'DD:HH'
64        'login_abuse_timeframe' => '04:00',
65
66        // The number of warnings a user will receive (and their password reset each time) before their account is completely blocked.
67        'login_abuse_warnings' => 3,
68
69        // The maximum number of IP addresses a user can login with over the timeout period before their account is blocked.
70        'login_abuse_max_ips' => 5,
71
72        // The IP address subnet size threshold. Uses a CIDR notation network mask (see CIDR cheatsheet at bottom).
73        // Any integar between 0 and 32 is permitted. Setting this to '24' permits any address in a
74        // class C network (255.255.255.0) to be considered the same. Setting to '32' compares each IP absolutely.
75        // Setting to '0' ignores all IPs, thus disabling login_abuse checking.
76        'login_abuse_ip_bitmask' => 32,
77
78        // Specify usernames to exclude from the account abuse detection system. This is specified as a hardcoded array provided at
79        // class instantiation time, or can be saved in the db_table under the login_abuse_exempt field.
80        'login_abuse_exempt_usernames' => array(),
81
82        // An array of IP blocks that are bypass the remote_ip comparison check. Useful for dynamic IPs or those behind proxy servers.
83        'trusted_networks' => array(),
84
85        // Allow user accounts to be blocked? Requires the user table to have the columns 'blocked' and 'blocked_reason'
86        'blocking' => false,
87
88        // Use a db_login_table to detect excessive logins. This requires blocking to be enabled.
89        'abuse_detection' => false,
90    );
91
92    /**
93     * Constructs a new authentication object.
94     *
95     * @access public
96     * @param optional array $params  A hash containing parameters.
97     */
98    function Auth_SQL($namespace='')
99    {
100        $app =& App::getInstance();
101       
102        $this->_ns = $namespace;
103
104        // Initialize default parameters.
105        $this->setParam($this->_default_params);
106
107        // Get create tables config from global context.
108        if (!is_null($app->getParam('db_create_tables'))) {
109            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
110        }
111    }
112
113    /**
114     * Setup the database tables for this class.
115     *
116     * @access  public
117     * @author  Quinn Comendant <quinn@strangecode.com>
118     * @since   26 Aug 2005 17:09:36
119     */
120    function initDB($recreate_db=false)
121    {
122        $app =& App::getInstance();
123        $db =& DB::getInstance();
124   
125   
126        static $_db_tested = false;
127
128        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
129
130            // User table.
131            if ($recreate_db) {
132                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
133                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_DEBUG, __FILE__, __LINE__);
134            }
135
136            // The minimal columns for a table compatable with the Auth_SQL class.
137            $db->query("CREATE TABLE IF NOT EXISTS " . $db->escapeString($this->getParam('db_table')) . " (
138                " . $this->getParam('db_primary_key') . " smallint(11) NOT NULL auto_increment,
139                " . $this->getParam('db_username_column') . " varchar(255) NOT NULL default '',
140                userpass varchar(255) NOT NULL default '',
141                first_name varchar(255) NOT NULL default '',
142                last_name varchar(255) NOT NULL default '',
143                email varchar(255) NOT NULL default '',
144                user_type enum('public', 'editor', 'admin', 'root') default NULL,
145                login_abuse_exempt enum('true') default NULL,
146                blocked enum('true') default NULL,
147                blocked_reason varchar(255) NOT NULL default '',
148                abuse_warning_level tinyint(4) NOT NULL default '0',
149                seconds_online int(11) NOT NULL default '0',
150                last_login_datetime datetime NOT NULL default '0000-00-00 00:00:00',
151                last_access_datetime datetime NOT NULL default '0000-00-00 00:00:00',
152                last_login_ip varchar(255) NOT NULL default '0.0.0.0',
153                added_by_user_id smallint(11) default NULL,
154                modified_by_user_id smallint(11) default NULL,
155                added_datetime datetime NOT NULL default '0000-00-00 00:00:00',
156                modified_datetime datetime NOT NULL default '0000-00-00 00:00:00',
157                PRIMARY KEY (" . $this->getParam('db_primary_key') . "),
158                KEY " . $this->getParam('db_username_column') . " (" . $this->getParam('db_username_column') . "),
159                KEY userpass (userpass),
160                KEY email (email)
161            )");
162
163            if (!$db->columnExists($this->getParam('db_table'), array(
164                $this->getParam('db_primary_key'),
165                $this->getParam('db_username_column'),
166                'userpass',
167                'first_name',
168                'last_name',
169                'email',
170                'user_type',
171                'login_abuse_exempt',
172                'blocked',
173                'blocked_reason',
174                'abuse_warning_level',
175                'seconds_online',
176                'last_login_datetime',
177                'last_access_datetime',
178                'last_login_ip',
179                'added_by_user_id',
180                'modified_by_user_id',
181                'added_datetime',
182                'modified_datetime',
183            ), false, false)) {
184                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
185                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
186            }
187
188            // Login table is used for abuse_detection features.
189            if ($this->getParam('abuse_detection')) {
190                if ($recreate_db) {
191                    $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_login_table'));
192                    $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_login_table')), LOG_DEBUG, __FILE__, __LINE__);
193                }
194                $db->query("CREATE TABLE IF NOT EXISTS " . $this->getParam('db_login_table') . " (
195                    " . $this->getParam('db_primary_key') . " smallint(11) NOT NULL default '0',
196                    login_datetime datetime NOT NULL default '0000-00-00 00:00:00',
197                    remote_ip_binary char(32) NOT NULL default '',
198                    KEY " . $this->getParam('db_primary_key') . " (" . $this->getParam('db_primary_key') . "),
199                    KEY login_datetime (login_datetime),
200                    KEY remote_ip_binary (remote_ip_binary)
201                )");
202
203                if (!$db->columnExists($this->getParam('db_login_table'), array(
204                    $this->getParam('db_primary_key'),
205                    'login_datetime',
206                    'remote_ip_binary',
207                ), false, false)) {
208                    $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), LOG_ALERT, __FILE__, __LINE__);
209                    trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), E_USER_ERROR);
210                }
211            }
212        }
213        $_db_tested = true;
214    }
215
216    /**
217     * Set the params of an auth object.
218     *
219     * @param  array $params   Array of parameter keys and value to set.
220     * @return bool true on success, false on failure
221     */
222    function setParam($params)
223    {
224        if (isset($params) && is_array($params)) {
225            // Merge new parameters with old overriding only those passed.
226            $this->_params = array_merge($this->_params, $params);
227        }
228    }
229
230    /**
231     * Return the value of a parameter, if it exists.
232     *
233     * @access public
234     * @param string $param        Which parameter to return.
235     * @return mixed               Configured parameter value.
236     */
237    function getParam($param)
238    {
239        $app =& App::getInstance();
240   
241        if (isset($this->_params[$param])) {
242            return $this->_params[$param];
243        } else {
244            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
245            return null;
246        }
247    }
248
249    /**
250     * Clear any authentication tokens in the current session. A.K.A. logout.
251     *
252     * @access public
253     */
254    function clear()
255    {
256        $db =& DB::getInstance();
257   
258        $this->initDB();
259
260        $db->query("
261            UPDATE " . $this->_params['db_table'] . " SET
262            seconds_online = seconds_online + (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)),
263            last_login_datetime = '0000-00-00 00:00:00'
264            WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
265        ");
266        $_SESSION['_auth_sql'][$this->_ns] = array('authenticated' => false);
267    }
268
269    /**
270     * Sets a variable into a registered auth session.
271     *
272     * @access public
273     * @param mixed $key      Which value to set.
274     * @param mixed $val      Value to set variable to.
275     */
276    function set($key, $val)
277    {
278        if (!isset($_SESSION['_auth_sql'][$this->_ns]['user_data'])) {
279            $_SESSION['_auth_sql'][$this->_ns]['user_data'] = array();
280        }
281        $_SESSION['_auth_sql'][$this->_ns]['user_data'][$key] = $val;
282    }
283
284    /**
285     * Returns a specified value from a registered auth session.
286     *
287     * @access public
288     * @param mixed $key      Which value to return.
289     * @param mixed $default  Value to return if key not found in user_data.
290     * @return mixed          Value stored in session.
291     */
292    function get($key, $default='')
293    {
294        if (isset($_SESSION['_auth_sql'][$this->_ns][$key])) {
295            return $_SESSION['_auth_sql'][$this->_ns][$key];
296        } else if (isset($_SESSION['_auth_sql'][$this->_ns]['user_data'][$key])) {
297            return $_SESSION['_auth_sql'][$this->_ns]['user_data'][$key];
298        } else {
299            return $default;
300        }
301    }
302
303    /**
304     * Find out if a set of login credentials are valid.
305     *
306     * @access private
307     * @param string $username      The username to check.
308     * @param string $password      The password to compare to username.
309     * @return mixed  False if credentials not found in DB, or returns DB row matching credentials.
310     */
311    function authenticate($username, $password)
312    {
313        $app =& App::getInstance();
314        $db =& DB::getInstance();
315
316        $this->initDB();
317
318        switch ($this->_params['encryption_type']) {
319        case AUTH_ENCRYPT_CRYPT :
320            // Query DB for user matching credentials. Compare cyphertext with salted-encrypted password.
321            $qid = $db->query("
322                SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
323                FROM " . $this->_params['db_table'] . "
324                WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
325                AND BINARY userpass = ENCRYPT('" . $db->escapeString($password) . "', LEFT(userpass, 2)))
326            ");
327            break;
328        case AUTH_ENCRYPT_PLAINTEXT :
329        case AUTH_ENCRYPT_MD5 :
330        case AUTH_ENCRYPT_SHA1 :
331        default :
332            // Query DB for user matching credentials. Directly compare cyphertext with result from encryptPassword().
333            $qid = $db->query("
334                SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
335                FROM " . $this->_params['db_table'] . "
336                WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
337                AND BINARY userpass = '" . $db->escapeString($this->encryptPassword($password)) . "'
338            ");
339            break;
340        }
341
342        // Return user data if found.
343        if ($user_data = mysql_fetch_assoc($qid)) {
344            // Don't return password value.
345            unset($user_data['userpass']);
346            $app->logMsg(sprintf('Authentication successful for user %s (%s)', $user_data['user_id'], $username), LOG_INFO, __FILE__, __LINE__);
347            return $user_data;
348        } else {
349            $app->logMsg(sprintf('Authentication failed for user %s (encrypted attempted password: %s)', $username, $this->encryptPassword($password)), LOG_NOTICE, __FILE__, __LINE__);
350            return false;
351        }
352    }
353
354    /**
355     * If user authenticated, register login into session.
356     *
357     * @access private
358     * @param string $username     The username to check.
359     * @param string $password     The password to compare to username.
360     * @return boolean  Whether or not the credentials are valid.
361     */
362    function login($username, $password)
363    {
364        $app =& App::getInstance();
365        $db =& DB::getInstance();
366   
367        $this->initDB();
368
369        $this->clear();
370
371        if (!$user_data = $this->authenticate($username, $password)) {
372            // No login: failed authentication!
373            return false;
374        }
375
376        // Register authenticated session.
377        $_SESSION['_auth_sql'][$this->_ns] = array(
378            'authenticated'         => true,
379            'user_id'               => $user_data['user_id'],
380            'username'              => $username,
381            'login_datetime'        => date('Y-m-d H:i:s'),
382            'last_access_datetime'  => date('Y-m-d H:i:s'),
383            'remote_ip'             => getRemoteAddr(),
384            'login_abuse_exempt'    => isset($user_data['login_abuse_exempt']) ? !empty($user_data['login_abuse_exempt']) : in_array($username, $this->_params['login_abuse_exempt_usernames']),
385            'user_data'             => $user_data
386        );
387
388        /**
389         * Check if the account is blocked, respond in context to reason. Cancel the login if blocked.
390         */
391        if ($this->getParam('blocking')) {
392            if (!empty($user_data['blocked'])) {
393
394                $app->logMsg(sprintf('User %s (%s) login failed due to blocked account: %s', $this->get('user_id'), $this->get('username'), $this->get('blocked_reason')), LOG_NOTICE, __FILE__, __LINE__);
395
396                switch ($user_data['blocked_reason']) {
397                    case 'account abuse' :
398                        $app->raiseMsg(sprintf(_("This account has been blocked due to possible account abuse. Please contact us to reactivate."), null), MSG_WARNING, __FILE__, __LINE__);
399                        break;
400                    default :
401                        $app->raiseMsg(sprintf(_("This account is currently not active. %s"), $user_data['blocked_reason']), MSG_WARNING, __FILE__, __LINE__);
402                        break;
403                }
404
405                // No login: user is blocked!
406                $this->clear();
407                return false;
408            }
409        }
410
411        /**
412         * Check the db_login_table for too many logins under this account.
413         * (1) Count the number of unique IP addresses that logged in under this user within the login_abuse_timeframe
414         * (2) If this number exceeds the login_abuse_max_ips, assume multiple people are logging in under the same account.
415        **/
416        if ($this->getParam('abuse_detection') && !$this->get('login_abuse_exempt')) {
417            $qid = $db->query("
418                SELECT COUNT(DISTINCT LEFT(remote_ip_binary, " . $this->_params['login_abuse_ip_bitmask'] . "))
419                FROM " . $this->_params['db_login_table'] . "
420                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
421                AND DATE_ADD(login_datetime, INTERVAL '" . $this->_params['login_abuse_timeframe'] . "' DAY_HOUR) > NOW()
422            ");
423            list($distinct_ips) = mysql_fetch_row($qid);
424            if ($distinct_ips > $this->_params['login_abuse_max_ips']) {
425                if ($this->get('abuse_warning_level') < $this->_params['login_abuse_warnings']) {
426                    // Warn the user with a password reset.
427                    $this->resetPassword(null, _("This is a security precaution. We have detected this account has been accessed from multiple computers simultaneously. It is against policy to share login information with others. If further account abuse is detected this account will be blocked."));
428                    $app->raiseMsg(_("Your password has been reset as a security precaution. Please check your email for more information."), MSG_NOTICE, __FILE__, __LINE__);
429                    $app->logMsg(sprintf('Account abuse detected for user %s (%s) from IP %s', $this->get('user_id'), $this->get('username'), $this->get('remote_ip')), LOG_WARNING, __FILE__, __LINE__);
430                } else {
431                    // Block the account with the reason of account abuse.
432                    $this->blockAccount(null, 'account abuse');
433                    $app->raiseMsg(_("Your account has been blocked as a security precaution. Please contact us for more information."), MSG_NOTICE, __FILE__, __LINE__);
434                    $app->logMsg(sprintf('Account blocked for user %s (%s) from IP %s', $this->get('user_id'), $this->get('username'), $this->get('remote_ip')), LOG_ALERT, __FILE__, __LINE__);
435                }
436                // Increment user's warning level.
437                $db->query("UPDATE " . $this->_params['db_table'] . " SET abuse_warning_level = abuse_warning_level + 1 WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'");
438                // Reset the login counter for this user.
439                $db->query("DELETE FROM " . $this->_params['db_login_table'] . " WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'");
440                // No login: reset password because of account abuse!
441                $this->clear();
442                return false;
443            }
444
445            // Update the login counter table with this login access. Convert IP to binary.
446            // TODO: after MySQL 5.0.23 is released this query could benefit from INSERT DELAYED.
447            $db->query("
448                INSERT INTO " . $this->_params['db_login_table'] . " (
449                    " . $this->_params['db_primary_key'] . ",
450                    login_datetime,
451                    remote_ip_binary
452                ) VALUES (
453                    '" . $this->get('user_id') . "',
454                    '" . $this->get('login_datetime') . "',
455                    '" . sprintf('%032b', ip2long($this->get('remote_ip'))) . "'
456                )
457            ");
458        }
459
460        // Update user table with this login.
461        $db->query("
462            UPDATE " . $this->_params['db_table'] . " SET
463                last_login_datetime = '" . $this->get('login_datetime') . "',
464                last_access_datetime = '" . $this->get('login_datetime') . "',
465                last_login_ip = '" . $this->get('remote_ip') . "'
466            WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
467        ");
468
469        // We're logged-in!
470        return true;
471    }
472
473    /**
474     * Test if user has a currently logged-in session.
475     *  - authentication flag set to true
476     *  - username not empty
477     *  - total logged-in time is not greater than login_timeout
478     *  - idle time is not greater than idle_timeout
479     *  - remote address is the same as the login remote address (aol users excluded).
480     *
481     * @access public
482     */
483    function isLoggedIn($user_id=null)
484    {
485        $app =& App::getInstance();
486        $db =& DB::getInstance();
487   
488        $this->initDB();
489
490        if (isset($user_id)) {
491            // Check the login status of a specific user.
492            $qid = $db->query("
493                SELECT 1 FROM " . $this->_params['db_table'] . "
494                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
495                AND DATE_ADD(last_login_datetime, INTERVAL '" . $this->_params['login_timeout'] . "' SECOND) > NOW()
496                AND DATE_ADD(last_access_datetime, INTERVAL '" . $this->_params['idle_timeout'] . "' SECOND) > NOW()
497            ");
498            return (mysql_num_rows($qid) > 0);
499        }
500
501        // User login test need only be run once per script execution. We cache the result in the session.
502        if ($this->_authentication_tested && isset($_SESSION['_auth_sql'][$this->_ns]['authenticated'])) {
503            return $_SESSION['_auth_sql'][$this->_ns]['authenticated'];
504        }
505
506        // Tesing login should occur once. This is the first time. Set flag.
507        $this->_authentication_tested = true;
508
509        // Some users will access from networks with a changing IP number (i.e. behind a proxy server). These users must be allowed entry by adding their IP to the list of trusted_networks.
510        if ($trusted_net = ipInRange(getRemoteAddr(), $this->_params['trusted_networks'])) {
511            $user_in_trusted_network = true;
512            $app->logMsg(sprintf('User %s accessing from trusted network %s',
513                ($this->get('user_id') ? ' ' . $this->get('user_id') . ' (' .  $this->get('username') . ')' : ''),
514                $trusted_net
515            ), LOG_DEBUG, __FILE__, __LINE__);
516        } else if (preg_match('/proxy.aol.com$/i', getRemoteAddr(true))) {
517            $user_in_trusted_network = true;
518            $app->logMsg(sprintf('User %s accessing from trusted network proxy.aol.com',
519                ($this->get('user_id') ? ' ' . $this->get('user_id') . ' (' .  $this->get('username') . ')' : '')
520            ), LOG_DEBUG, __FILE__, __LINE__);
521        } else {
522            $user_in_trusted_network = false;
523        }
524
525        // Test login with information stored in session. Skip IP matching for users from trusted networks.
526        if (isset($_SESSION['_auth_sql'][$this->_ns])
527            && true === $_SESSION['_auth_sql'][$this->_ns]['authenticated']
528            && !empty($_SESSION['_auth_sql'][$this->_ns]['username'])
529            && strtotime($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) > time() - $this->_params['login_timeout']
530            && strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) > time() - $this->_params['idle_timeout']
531            && ($_SESSION['_auth_sql'][$this->_ns]['remote_ip'] == getRemoteAddr() || $user_in_trusted_network)
532        ) {
533            // User is authenticated!
534            $_SESSION['_auth_sql'][$this->_ns]['last_access_datetime'] = date('Y-m-d H:i:s');
535
536            // Update the DB with the last_access_datetime and increment the seconds_online.
537            $db->query("
538                UPDATE " . $this->_params['db_table'] . " SET
539                seconds_online = seconds_online + (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)) + 1,
540                last_access_datetime = '" . $this->get('last_access_datetime') . "'
541                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
542            ");
543            if (mysql_affected_rows($db->getDBH()) > 0) {
544                // User record still exists in DB. Do this to ensure user was not delete from DB between accesses. Notice "+ 1" in SQL above to ensure record is modified.
545                return true;
546            } else {
547                $app->logMsg(sprintf('User update failed. Record not found for user %s (%s).', $this->get('user_id'), $this->get('username')), LOG_NOTICE, __FILE__, __LINE__);
548            }
549        } else if (isset($_SESSION['_auth_sql'][$this->_ns]) && true === $_SESSION['_auth_sql'][$this->_ns]['authenticated']) {
550            // User is authenticated, but login has expired.
551            if (strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) > time() - 43200) {
552                // Only raise message if last session is less than 12 hours old.
553                $app->raiseMsg(_("Your session has expired. You need to log-in again."), MSG_NOTICE, __FILE__, __LINE__);
554            }
555
556            // Log the reason for login expiration.
557            $expire_reasons = array();
558            if (empty($_SESSION['_auth_sql'][$this->_ns]['username'])) {
559                $expire_reasons[] = 'username not found';
560            }
561            if (strtotime($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) <= time() - $this->_params['login_timeout']) {
562                $expire_reasons[] = 'login_timeout expired';
563            }
564            if (strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) <= time() - $this->_params['idle_timeout']) {
565                $expire_reasons[] = 'idle_timeout expired';
566            }
567            if ($_SESSION['_auth_sql'][$this->_ns]['remote_ip'] != getRemoteAddr() && !$user_in_trusted_network) {
568                $expire_reasons[] = sprintf('remote_ip not matched (%s != %s)', $_SESSION['_auth_sql'][$this->_ns]['remote_ip'], getRemoteAddr());
569            }
570            $app->logMsg(sprintf('User %s (%s) session expired: %s', $this->get('user_id'), $this->get('username'), join(', ', $expire_reasons)), LOG_INFO, __FILE__, __LINE__);
571        }
572
573        // User is not authenticated.
574        $this->clear();
575        return false;
576    }
577
578    /**
579     * Redirect user to login page if they are not logged in.
580     *
581     * @param string $message The text description of a message to raise.
582     * @param int    $type    The type of message: MSG_NOTICE,
583     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
584     * @param string $file    __FILE__.
585     * @param string $line    __LINE__.
586     * @access public
587     */
588    function requireLogin($message='', $type=MSG_NOTICE, $file=null, $line=null)
589    {
590        $app =& App::getInstance();
591   
592        if (!$this->isLoggedIn()) {
593            // Display message for requiring login. (RaiseMsg will ignore empty strings.)
594            $app->raiseMsg($message, $type, $file, $line);
595
596            // Login scripts must have the same 'login' tag for boomerangURL verification/manipulation.
597            $app->setBoomerangURL(absoluteMe(), 'login');
598            $app->dieURL($this->_params['login_url']);
599        }
600    }
601
602    /**
603     * This sets the 'blocked' field for a user in the db_table, and also
604     * adds an optional reason
605     *
606     * @param  string   $reason      The reason for blocking the account.
607     */
608    function blockAccount($user_id=null, $reason='')
609    {
610        $app =& App::getInstance();
611        $db =& DB::getInstance();
612   
613        $this->initDB();
614
615        if ($this->getParam('blocking')) {
616            if (strlen($db->escapeString($reason)) > 255) {
617                // blocked_reason field is varchar(255).
618                $app->logMsg(sprintf('Blocked reason provided is greater than 255 characters: %s', $reason), LOG_WARNING, __FILE__, __LINE__);
619            }
620
621            // Get user_id if specified.
622            $user_id = isset($user_id) ? $user_id : $this->get('user_id');
623            $db->query("
624                UPDATE " . $this->_params['db_table'] . " SET
625                blocked = 'true',
626                blocked_reason = '" . $db->escapeString($reason) . "'
627                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
628            ");
629        }
630    }
631
632    /**
633     * Unblocks a user in the db_table, and clears any blocked_reason.
634     */
635    function unblockAccount($user_id=null)
636    {
637        $db =& DB::getInstance();
638   
639        $this->initDB();
640   
641        if ($this->getParam('blocking')) {
642            // Get user_id if specified.
643            $user_id = isset($user_id) ? $user_id : $this->get('user_id');
644            $db->query("
645                UPDATE " . $this->_params['db_table'] . " SET
646                blocked = '',
647                blocked_reason = ''
648                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
649            ");
650        }
651    }
652
653    /**
654     * Returns true if username already exists in database.
655     *
656     * @param  string  $username    Username to look for.
657     * @return bool                 True if username exists.
658     */
659    function usernameExists($username)
660    {
661        $db =& DB::getInstance();
662   
663        $this->initDB();
664
665        $qid = $db->query("
666            SELECT 1
667            FROM " . $this->_params['db_table'] . "
668            WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
669        ");
670        return (mysql_num_rows($qid) > 0);
671    }
672
673    /**
674     * Returns a username for a specified user id.
675     *
676     * @param  string  $user_id     User id to look for.
677     * @return string               Username, or false if none found.
678     */
679    function getUsername($user_id)
680    {
681        $db =& DB::getInstance();
682   
683        $this->initDB();
684
685        $qid = $db->query("
686            SELECT " . $this->_params['db_username_column'] . "
687            FROM " . $this->_params['db_table'] . "
688            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
689        ");
690        if (list($username) = mysql_fetch_row($qid)) {
691            return $username;
692        } else {
693            return false;
694        }
695    }
696
697    /**
698     * Returns a randomly generated password based on $pattern. The pattern is any
699     * sequence of 'x', 'V', 'C', 'v', 'c', or 'd' and if it is something like 'cvccv' this
700     * function will generate a pronouncable password. Recommend using more complex
701     * patterns, at minimum the US State Department standard: cvcddcvc.
702     *
703     * - x    a random upper or lower alpha character or digit
704     * - C    a random upper or lower consanant
705     * - V    a random upper or lower vowel
706     * - c    a random lowercase consanant
707     * - v    a random lowercase vowel
708     * - d    a random digit
709     *
710     * @param  string $pattern  a sequence of character types, above.
711     * @return string           a password
712     */
713    function generatePassword($pattern='CvcdCvc')
714    {
715        $app =& App::getInstance();
716        if (preg_match('/[^xCVcvd]/', $pattern)) {
717            $app->logMsg(sprintf('Invalid pattern: %s', $pattern), LOG_WARNING, __FILE__, __LINE__);
718            $pattern='CvcdCvc';
719        }
720        $str = '';
721        for ($i=0; $i<strlen($pattern); $i++) {
722            $x = substr('bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZaeiouyAEIOUY0123456789', (mt_rand() % 60), 1);
723            $c = substr('bcdfghjklmnprstvwxz', (mt_rand() % 19), 1);
724            $C = substr('bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZ', (mt_rand() % 38), 1);
725            $v = substr('aeiouy', (mt_rand() % 6), 1);
726            $V = substr('aeiouyAEIOUY', (mt_rand() % 12), 1);
727            $d = substr('0123456789', (mt_rand() % 10), 1);
728            $str .= $$pattern{$i};
729        }
730        return $str;
731    }
732
733    /**
734     *
735     */
736    function encryptPassword($password, $salt=null)
737    {
738        $app =& App::getInstance();
739       
740        // Existing password hashes rely on the same key/salt being used to compare encryptions.
741        // Don't change this unless you know existing hashes or signatures will not be affected!
742        $more_salt = 'B36D18E5-3FE4-4D58-8150-F26642852B81';
743       
744        switch ($this->_params['encryption_type']) {
745        case AUTH_ENCRYPT_PLAINTEXT :
746            return $password;
747            break;
748
749        case AUTH_ENCRYPT_CRYPT :
750            // If comparing plaintext password with a hash, provide first two chars of the hash as the salt.
751            return isset($salt) ? crypt($password, substr($salt, 0, 2)) : crypt($password);
752            break;
753
754        case AUTH_ENCRYPT_SHA1 :
755            return sha1($password);
756            break;
757
758        case AUTH_ENCRYPT_SHA1_HARDENED :
759            $hash = sha1($app->getParam('signing_key') . $password . $more_salt);
760            // Increase key strength by 12 bits.
761            for ($i=0; $i < 4096; $i++) { 
762                $hash = sha1($hash); 
763            } 
764            return $hash;
765            break;
766
767        case AUTH_ENCRYPT_MD5 :
768            return md5($password);
769            break;
770
771        case AUTH_ENCRYPT_MD5_HARDENED :
772            // Include salt to improve hash
773            $hash = md5($app->getParam('signing_key') . $password . $more_salt);
774            // Increase key strength by 12 bits.
775            for ($i=0; $i < 4096; $i++) { 
776                $hash = md5($hash); 
777            } 
778            return $hash;
779            break;
780        default :
781            $app->logMsg(sprintf('Authentication encrypt type specified is unrecognized: %s', $this->_params['encryption_type']), LOG_NOTICE, __FILE__, __LINE__);
782            return false;
783            break;
784        }
785    }
786
787    /**
788     *
789     */
790    function setPassword($user_id=null, $password)
791    {
792        $app =& App::getInstance();
793        $db =& DB::getInstance();
794   
795        $this->initDB();
796
797        // Get user_id if specified.
798        $user_id = isset($user_id) ? $user_id : $this->get('user_id');
799
800        // Issue the password change query.
801        $db->query("
802            UPDATE " . $this->_params['db_table'] . "
803            SET userpass = '" . $db->escapeString($this->encryptPassword($password)) . "'
804            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
805        ");
806       
807        if (mysql_affected_rows($db->getDBH()) != 1) {
808            $app->logMsg(sprintf('setPassword failed to update password for user %s', $user_id), LOG_NOTICE, __FILE__, __LINE__);
809        }
810    }
811
812    /**
813     * Resets the password for the user with the specified id.
814     *
815     * @param  string $user_id   The id of the user to reset.
816     * @param  string $reason    Additional message to add to the reset email.
817     * @return string            The user's new password.
818     */
819    function resetPassword($user_id=null, $reason='')
820    {
821        $app =& App::getInstance();
822        $db =& DB::getInstance();
823   
824        $this->initDB();
825
826        // Get user_id if specified.
827        $user_id = isset($user_id) ? $user_id : $this->get('user_id');
828
829        // Reset password of a specific user.
830        $qid = $db->query("
831            SELECT * FROM " . $this->_params['db_table'] . "
832            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
833        ");
834        if (!$user_data = mysql_fetch_assoc($qid)) {
835            $app->logMsg(sprintf('Reset password failed. User %s not found.', $user_id), LOG_NOTICE, __FILE__, __LINE__);
836            return false;
837        }
838
839        // Get new password.
840        $password = $this->generatePassword();
841
842        // Update password query.
843        $this->setPassword($user_id, $password);
844
845        // Make sure user has an email on record before continuing.
846        if (!isset($user_data['email']) || '' == trim($user_data['email'])) {
847            $app->logMsg(sprintf('Password reset but notification failed, no email address for user %s (%s).', $user_data[$this->_params['db_primary_key']], $user_data[$this->_params['db_username_column']]), LOG_NOTICE, __FILE__, __LINE__);
848        } else {
849            // Body for email.
850            $email_body = <<<EOF
851Hello {NAME},
852
853Your password at {SITE_NAME} has been reset. {REASON}
854Your new login information is:
855
856USERNAME: {USERNAME}
857PASSWORD: {PASSWORD}
858
859If you have any questions or concerns please reply to this email or visit the website below.
860
861Thank you,
862{SITE_NAME}
863{SITE_URL}/
864
865EOF;
866            $email = new Email(array(
867                'to' => $user_data['email'],
868                'from' => sprintf('%s <%s>', $app->getParam('site_name'), $app->getParam('site_email')),
869                'subject' => sprintf('%s password change', $app->getParam('site_name'))
870            ));
871            $email->setString($email_body);
872            $email->replace(array(
873                'site_name' => $app->getParam('site_name'),
874                'site_url' => $app->getParam('site_url'),
875                'name' => ('' != $user_data['first_name'] . $user_data['last_name'] ? $user_data['first_name'] . ' ' . $user_data['last_name'] : $user_data[$this->_params['db_username_column']]),
876                'username' => $user_data[$this->_params['db_username_column']],
877                'password' => $password,
878                'reason' => $reason,
879            ));
880            $email->send();
881        }
882
883        return array(
884            'username' => $user_data[$this->_params['db_username_column']],
885            'userpass' => $password
886        );
887    }
888
889    /**
890     * If the current user has access to the specified $security_zone, return true.
891     * If the optional $priv is supplied, test that against the zone.
892     *
893     * @param  constant $security_zone   string of comma delimited priviliges for the zone
894     * @param  string   $priv            a privilege that might be found in a zone
895     * @return bool     true if user is a member of security zone, false otherwise
896     */
897    function inClearanceZone($security_zone, $priv='')
898    {
899        return true;
900        $zone_members = preg_split('/,\s*/', $security_zone);
901        $priv = empty($priv) ? $this->get('priv') : $priv;
902
903        // If the current user's privilege level is NOT in that array or if the
904        // user has no privilege, return false. Otherwise the user is clear.
905        if (!in_array($priv, $zone_members) || empty($priv)) {
906            return false;
907        } else {
908            return true;
909        }
910    }
911
912    /**
913     * This function tests a list of arguments $security_zone against the priv that the current user has.
914     * If the user doesn't have one of the supplied privs, die.
915     *
916     * @param  constant $security_zone   string of comma delimited priviliges for the zone
917     */
918    function requireAccessClearance($security_zone, $message='')
919    {
920        $app =& App::getInstance();
921   
922        return true;
923        $zone_members = preg_split('/,\s*/', $security_zone);
924
925        /* If the current user's privilege level is NOT in that array or if the
926         * user has no privilege, DIE with a message. */
927        if (!in_array($this->get('priv'), $zone_members) || !$this->get('priv')) {
928            $message = empty($message) ? _("You have insufficient privileges to view that page.") : $message;
929            $app->raiseMsg($message, MSG_NOTICE, __FILE__, __LINE__);
930            $app->dieBoomerangURL();
931        }
932    }
933
934} // end class
935
936// CIDR cheat-sheet
937//
938// Netmask              Netmask (binary)                 CIDR     Notes
939// _____________________________________________________________________________
940// 255.255.255.255  11111111.11111111.11111111.11111111  /32  Host (single addr)
941// 255.255.255.254  11111111.11111111.11111111.11111110  /31  Unusable
942// 255.255.255.252  11111111.11111111.11111111.11111100  /30    2  useable
943// 255.255.255.248  11111111.11111111.11111111.11111000  /29    6  useable
944// 255.255.255.240  11111111.11111111.11111111.11110000  /28   14  useable
945// 255.255.255.224  11111111.11111111.11111111.11100000  /27   30  useable
946// 255.255.255.192  11111111.11111111.11111111.11000000  /26   62  useable
947// 255.255.255.128  11111111.11111111.11111111.10000000  /25  126  useable
948// 255.255.255.0    11111111.11111111.11111111.00000000  /24 "Class C" 254 useable
949//
950// 255.255.254.0    11111111.11111111.11111110.00000000  /23    2  Class C's
951// 255.255.252.0    11111111.11111111.11111100.00000000  /22    4  Class C's
952// 255.255.248.0    11111111.11111111.11111000.00000000  /21    8  Class C's
953// 255.255.240.0    11111111.11111111.11110000.00000000  /20   16  Class C's
954// 255.255.224.0    11111111.11111111.11100000.00000000  /19   32  Class C's
955// 255.255.192.0    11111111.11111111.11000000.00000000  /18   64  Class C's
956// 255.255.128.0    11111111.11111111.10000000.00000000  /17  128  Class C's
957// 255.255.0.0      11111111.11111111.00000000.00000000  /16  "Class B"
958//
959// 255.254.0.0      11111111.11111110.00000000.00000000  /15    2  Class B's
960// 255.252.0.0      11111111.11111100.00000000.00000000  /14    4  Class B's
961// 255.248.0.0      11111111.11111000.00000000.00000000  /13    8  Class B's
962// 255.240.0.0      11111111.11110000.00000000.00000000  /12   16  Class B's
963// 255.224.0.0      11111111.11100000.00000000.00000000  /11   32  Class B's
964// 255.192.0.0      11111111.11000000.00000000.00000000  /10   64  Class B's
965// 255.128.0.0      11111111.10000000.00000000.00000000  /9   128  Class B's
966// 255.0.0.0        11111111.00000000.00000000.00000000  /8   "Class A"
967//
968// 254.0.0.0        11111110.00000000.00000000.00000000  /7
969// 252.0.0.0        11111100.00000000.00000000.00000000  /6
970// 248.0.0.0        11111000.00000000.00000000.00000000  /5
971// 240.0.0.0        11110000.00000000.00000000.00000000  /4
972// 224.0.0.0        11100000.00000000.00000000.00000000  /3
973// 192.0.0.0        11000000.00000000.00000000.00000000  /2
974// 128.0.0.0        10000000.00000000.00000000.00000000  /1
975// 0.0.0.0          00000000.00000000.00000000.00000000  /0   IP space
976?>
Note: See TracBrowser for help on using the repository browser.