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

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

Q - removed user_type from auth_sql.

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