* Copyright 2001-2010 Strangecode, LLC * * This file is part of The Strangecode Codebase. * * The Strangecode Codebase is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your option) * any later version. * * The Strangecode Codebase is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * The Strangecode Codebase. If not, see . */ /* * The Auth_SQL class provides a SQL implementation for authentication. * * @author Quinn Comendant * @version 2.1 */ // Available encryption types for class Auth_SQL. define('AUTH_ENCRYPT_PLAINTEXT', 1); define('AUTH_ENCRYPT_CRYPT', 2); define('AUTH_ENCRYPT_SHA1', 3); define('AUTH_ENCRYPT_SHA1_HARDENED', 4); define('AUTH_ENCRYPT_MD5', 5); define('AUTH_ENCRYPT_MD5_HARDENED', 6); require_once dirname(__FILE__) . '/Email.inc.php'; class Auth_SQL { // Namespace of this auth object. var $_ns; // Static var for test. var $_authentication_tested; // Parameters to be configured by setParam. var $_params = array(); var $_default_params = array( // Automatically create table and verify columns. Better set to false after site launch. 'create_table' => true, // The database table containing users to authenticate. 'db_table' => 'user_tbl', // The name of the primary key for the db_table. 'db_primary_key' => 'user_id', // The name of the username key for the db_table. 'db_username_column' => 'username', // If using the db_login_table feature, specify the db_login_table. The primary key must match the primary key for the db_table. 'db_login_table' => 'user_login_tbl', // The type of encryption to use for passwords stored in the db_table. Use one of the AUTH_ENCRYPT_* types specified above. // Hardened password hashes rely on the same key/salt being used to compare encryptions. // Be aware that when using one of the hardened types the App signing_key or $more_salt below cannot change! 'encryption_type' => AUTH_ENCRYPT_MD5, // The URL to the login script. 'login_url' => '/', // The maximum amount of time a user is allowed to be logged in. They will be forced to login again if they expire. // In seconds. 21600 seconds = 6 hours. 'login_timeout' => 21600, // 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. // In seconds. 3600 seconds = 1 hour. 'idle_timeout' => 3600, // 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. // Days and hours, like this: 'DD:HH' 'login_abuse_timeframe' => '04:00', // The number of warnings a user will receive (and their password reset each time) before their account is completely blocked. 'login_abuse_warnings' => 3, // The maximum number of IP addresses a user can login with over the timeout period before their account is blocked. 'login_abuse_max_ips' => 5, // The IP address subnet size threshold. Uses a CIDR notation network mask (see CIDR cheat-sheet at bottom). // Any integer between 0 and 32 is permitted. Setting this to '24' permits any address in a // class C network (255.255.255.0) to be considered the same. Setting to '32' compares each IP absolutely. // Setting to '0' ignores all IPs, thus disabling login_abuse checking. 'login_abuse_ip_bitmask' => 32, // Specify usernames to exclude from the account abuse detection system. This is specified as a hardcoded array provided at // class instantiation time, or can be saved in the db_table under the login_abuse_exempt field. 'login_abuse_exempt_usernames' => array(), // Specify usernames to exclude from remote_ip matching. Users behind proxy servers should be appended to this array so their shifting remote IP will not log them out. 'match_remote_ip_exempt_usernames' => array(), // Match the user's current remote IP against the one they logged in with. 'match_remote_ip' => true, // An array of IP blocks that are bypass the remote_ip comparison check. Useful for dynamic IPs or those behind proxy servers. 'trusted_networks' => array(), // Allow user accounts to be blocked? Requires the user table to have the columns 'blocked' and 'blocked_reason' 'blocking' => false, // Use a db_login_table to detect excessive logins. This requires blocking to be enabled. 'abuse_detection' => false, ); /** * Constructs a new authentication object. * * @access public * @param optional array $params A hash containing parameters. */ function Auth_SQL($namespace='') { $app =& App::getInstance(); $this->_ns = $namespace; // Initialize default parameters. $this->setParam($this->_default_params); // Get create tables config from global context. if (!is_null($app->getParam('db_create_tables'))) { $this->setParam(array('create_table' => $app->getParam('db_create_tables'))); } if (!isset($_SESSION['_auth_sql'][$this->_ns])) { $this->clear(); } } /** * Setup the database tables for this class. * * @access public * @author Quinn Comendant * @since 26 Aug 2005 17:09:36 */ function initDB($recreate_db=false) { $app =& App::getInstance(); $db =& DB::getInstance(); static $_db_tested = false; if ($recreate_db || !$_db_tested && $this->getParam('create_table')) { // User table. if ($recreate_db) { $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table')); $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__); } // The minimal columns for a table compatable with the Auth_SQL class. $db->query("CREATE TABLE IF NOT EXISTS " . $db->escapeString($this->getParam('db_table')) . " ( " . $this->getParam('db_primary_key') . " smallint(11) NOT NULL auto_increment, " . $this->getParam('db_username_column') . " varchar(255) NOT NULL default '', userpass VARCHAR(255) NOT NULL DEFAULT '', first_name VARCHAR(255) NOT NULL DEFAULT '', last_name VARCHAR(255) NOT NULL DEFAULT '', email VARCHAR(255) NOT NULL DEFAULT '', login_abuse_exempt ENUM('TRUE') DEFAULT NULL, blocked ENUM('TRUE') DEFAULT NULL, blocked_reason VARCHAR(255) NOT NULL DEFAULT '', abuse_warning_level TINYINT(4) NOT NULL DEFAULT '0', seconds_online INT(11) NOT NULL DEFAULT '0', last_login_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', last_access_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', last_login_ip VARCHAR(255) NOT NULL DEFAULT '0.0.0.0', added_by_user_id SMALLINT(11) DEFAULT NULL, modified_by_user_id SMALLINT(11) DEFAULT NULL, added_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', modified_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (" . $this->getParam('db_primary_key') . "), KEY " . $this->getParam('db_username_column') . " (" . $this->getParam('db_username_column') . "), KEY userpass (userpass), KEY email (email) )"); if (!$db->columnExists($this->getParam('db_table'), array( $this->getParam('db_primary_key'), $this->getParam('db_username_column'), 'userpass', 'first_name', 'last_name', 'email', 'login_abuse_exempt', 'blocked', 'blocked_reason', 'abuse_warning_level', 'seconds_online', 'last_login_datetime', 'last_access_datetime', 'last_login_ip', 'added_by_user_id', 'modified_by_user_id', 'added_datetime', 'modified_datetime', ), false, false)) { $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__); trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR); } // Login table is used for abuse_detection features. if ($this->getParam('abuse_detection')) { if ($recreate_db) { $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_login_table')); $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_login_table')), LOG_INFO, __FILE__, __LINE__); } $db->query("CREATE TABLE IF NOT EXISTS " . $this->getParam('db_login_table') . " ( " . $this->getParam('db_primary_key') . " SMALLINT(11) NOT NULL DEFAULT '0', login_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', remote_ip_binary CHAR(32) NOT NULL DEFAULT '', KEY " . $this->getParam('db_primary_key') . " (" . $this->getParam('db_primary_key') . "), KEY login_datetime (login_datetime), KEY remote_ip_binary (remote_ip_binary) )"); if (!$db->columnExists($this->getParam('db_login_table'), array( $this->getParam('db_primary_key'), 'login_datetime', 'remote_ip_binary', ), false, false)) { $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), LOG_ALERT, __FILE__, __LINE__); trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), E_USER_ERROR); } } } $_db_tested = true; } /** * Set the params of an auth object. * * @param array $params Array of parameter keys and value to set. * @return bool true on success, false on failure */ function setParam($params) { if (isset($params['match_remote_ip_exempt_usernames'])) { $params['match_remote_ip_exempt_usernames'] = array_map('strtolower', $params['match_remote_ip_exempt_usernames']); } if (isset($params['login_abuse_exempt_usernames'])) { $params['login_abuse_exempt_usernames'] = array_map('strtolower', $params['login_abuse_exempt_usernames']); } if (isset($params) && is_array($params)) { // Merge new parameters with old overriding only those passed. $this->_params = array_merge($this->_params, $params); } } /** * Return the value of a parameter, if it exists. * * @access public * @param string $param Which parameter to return. * @return mixed Configured parameter value. */ function getParam($param) { $app =& App::getInstance(); if (isset($this->_params[$param])) { return $this->_params[$param]; } else { $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__); return null; } } /** * Clear any authentication tokens in the current session. A.K.A. logout. * * @access public */ function clear() { $db =& DB::getInstance(); $this->initDB(); if ($this->get('user_id', false)) { // FIX ME: Should we check if the session is active? $db->query(" UPDATE " . $this->_params['db_table'] . " SET seconds_online = seconds_online + (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)), last_login_datetime = '0000-00-00 00:00:00' WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "' "); } $_SESSION['_auth_sql'][$this->_ns] = array('authenticated' => false); } /** * Sets a variable into a registered auth session. * * @access public * @param mixed $key Which value to set. * @param mixed $val Value to set variable to. */ function set($key, $val) { if (!isset($_SESSION['_auth_sql'][$this->_ns]['user_data'])) { $_SESSION['_auth_sql'][$this->_ns]['user_data'] = array(); } $_SESSION['_auth_sql'][$this->_ns]['user_data'][$key] = $val; } /** * Returns a specified value from a registered auth session. * * @access public * @param mixed $key Which value to return. * @param mixed $default Value to return if key not found in user_data. * @return mixed Value stored in session. */ function get($key, $default='') { if (isset($_SESSION['_auth_sql'][$this->_ns][$key])) { return $_SESSION['_auth_sql'][$this->_ns][$key]; } else if (isset($_SESSION['_auth_sql'][$this->_ns]['user_data'][$key])) { return $_SESSION['_auth_sql'][$this->_ns]['user_data'][$key]; } else { return $default; } } /** * Find out if a set of login credentials are valid. * * @access private * @param string $username The username to check. * @param string $password The password to compare to username. * @return mixed False if credentials not found in DB, or returns DB row matching credentials. */ function authenticate($username, $password) { $app =& App::getInstance(); $db =& DB::getInstance(); $this->initDB(); switch ($this->_params['encryption_type']) { case AUTH_ENCRYPT_CRYPT : // Query DB for user matching credentials. Compare cyphertext with salted-encrypted password. $qid = $db->query(" SELECT *, " . $this->_params['db_primary_key'] . " AS user_id FROM " . $this->_params['db_table'] . " WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "' AND BINARY userpass = ENCRYPT('" . $db->escapeString($password) . "', LEFT(userpass, 2))) "); break; case AUTH_ENCRYPT_PLAINTEXT : case AUTH_ENCRYPT_MD5 : case AUTH_ENCRYPT_SHA1 : default : // Query DB for user matching credentials. Directly compare cyphertext with result from encryptPassword(). $qid = $db->query(" SELECT *, " . $this->_params['db_primary_key'] . " AS user_id FROM " . $this->_params['db_table'] . " WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "' AND BINARY userpass = '" . $db->escapeString($this->encryptPassword($password)) . "' "); break; } // Return user data if found. if ($user_data = mysql_fetch_assoc($qid)) { // Don't return password value. unset($user_data['userpass']); $app->logMsg(sprintf('Authentication successful for user_id %s (%s)', $user_data['user_id'], $username), LOG_INFO, __FILE__, __LINE__); return $user_data; } else { $app->logMsg(sprintf('Authentication failed for username %s (encrypted attempted password: %s)', $username, $this->encryptPassword($password)), LOG_NOTICE, __FILE__, __LINE__); return false; } } /** * If user authenticated, register login into session. * * @access private * @param string $username The username to check. * @param string $password The password to compare to username. * @return boolean Whether or not the credentials are valid. */ function login($username, $password) { $app =& App::getInstance(); $db =& DB::getInstance(); $this->initDB(); $this->clear(); if (!$user_data = $this->authenticate($username, $password)) { // No login: failed authentication! return false; } // Convert 'priv' to 'user_type' nomenclature. if (isset($user_data['priv'])) { $user_data['user_type'] = $user_data['priv']; } // Register authenticated session. $_SESSION['_auth_sql'][$this->_ns] = array( 'authenticated' => true, 'user_id' => $user_data['user_id'], 'username' => $username, 'login_datetime' => date('Y-m-d H:i:s'), 'last_access_datetime' => date('Y-m-d H:i:s'), 'remote_ip' => getRemoteAddr(), 'login_abuse_exempt' => isset($user_data['login_abuse_exempt']) ? !empty($user_data['login_abuse_exempt']) : in_array(strtolower($username), $this->_params['login_abuse_exempt_usernames']), 'match_remote_ip_exempt'=> isset($user_data['match_remote_ip_exempt']) ? !empty($user_data['match_remote_ip_exempt']) : in_array(strtolower($username), $this->_params['match_remote_ip_exempt_usernames']), 'user_data' => $user_data ); /** * Check if the account is blocked, respond in context to reason. Cancel the login if blocked. */ if ($this->getParam('blocking')) { if (!empty($user_data['blocked'])) { $app->logMsg(sprintf('User_id %s (%s) login failed due to blocked account: %s', $this->get('user_id'), $this->get('username'), $this->get('blocked_reason')), LOG_NOTICE, __FILE__, __LINE__); switch ($user_data['blocked_reason']) { case 'account abuse' : $app->raiseMsg(sprintf(_("This account has been blocked due to possible account abuse. Please contact us to reactivate."), null), MSG_WARNING, __FILE__, __LINE__); break; default : $app->raiseMsg(sprintf(_("This account is currently not active. %s"), $user_data['blocked_reason']), MSG_WARNING, __FILE__, __LINE__); break; } // No login: user is blocked! $this->clear(); return false; } } /** * Check the db_login_table for too many logins under this account. * (1) Count the number of unique IP addresses that logged in under this user within the login_abuse_timeframe * (2) If this number exceeds the login_abuse_max_ips, assume multiple people are logging in under the same account. **/ if ($this->getParam('abuse_detection') && !$this->get('login_abuse_exempt')) { $qid = $db->query(" SELECT COUNT(DISTINCT LEFT(remote_ip_binary, " . $this->_params['login_abuse_ip_bitmask'] . ")) FROM " . $this->_params['db_login_table'] . " WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "' AND DATE_ADD(login_datetime, INTERVAL '" . $this->_params['login_abuse_timeframe'] . "' DAY_HOUR) > NOW() "); list($distinct_ips) = mysql_fetch_row($qid); if ($distinct_ips > $this->_params['login_abuse_max_ips']) { if ($this->get('abuse_warning_level') < $this->_params['login_abuse_warnings']) { // Warn the user with a password reset. $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.")); $app->raiseMsg(_("Your password has been reset as a security precaution. Please check your email for more information."), MSG_NOTICE, __FILE__, __LINE__); $app->logMsg(sprintf('Account abuse detected for user_id %s (%s) from IP %s', $this->get('user_id'), $this->get('username'), $this->get('remote_ip')), LOG_WARNING, __FILE__, __LINE__); } else { // Block the account with the reason of account abuse. $this->blockAccount(null, 'account abuse'); $app->raiseMsg(_("Your account has been blocked as a security precaution. Please contact us for more information."), MSG_NOTICE, __FILE__, __LINE__); $app->logMsg(sprintf('Account blocked for user_id %s (%s) from IP %s', $this->get('user_id'), $this->get('username'), $this->get('remote_ip')), LOG_ALERT, __FILE__, __LINE__); } // Increment user's warning level. $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') . "'"); // Reset the login counter for this user. $db->query("DELETE FROM " . $this->_params['db_login_table'] . " WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'"); // No login: reset password because of account abuse! $this->clear(); return false; } // Update the login counter table with this login access. Convert IP to binary. // TODO: after MySQL 5.0.23 is released this query could benefit from INSERT DELAYED. $db->query(" INSERT INTO " . $this->_params['db_login_table'] . " ( " . $this->_params['db_primary_key'] . ", login_datetime, remote_ip_binary ) VALUES ( '" . $this->get('user_id') . "', '" . $this->get('login_datetime') . "', '" . sprintf('%032b', ip2long($this->get('remote_ip'))) . "' ) "); } // Update user table with this login. $db->query(" UPDATE " . $this->_params['db_table'] . " SET last_login_datetime = '" . $this->get('login_datetime') . "', last_access_datetime = '" . $this->get('login_datetime') . "', last_login_ip = '" . $this->get('remote_ip') . "' WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "' "); // We're logged-in! return true; } /** * Test if user has a currently logged-in session. * - authentication flag set to true * - username not empty * - total logged-in time is not greater than login_timeout * - idle time is not greater than idle_timeout * - remote address is the same as the login remote address (aol users excluded). * * @access public */ function isLoggedIn($user_id=null) { $app =& App::getInstance(); $db =& DB::getInstance(); $this->initDB(); if (isset($user_id)) { // Check the login status of a specific user. $qid = $db->query(" SELECT 1 FROM " . $this->_params['db_table'] . " WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "' AND DATE_ADD(last_login_datetime, INTERVAL '" . $this->_params['login_timeout'] . "' SECOND) > NOW() AND DATE_ADD(last_access_datetime, INTERVAL '" . $this->_params['idle_timeout'] . "' SECOND) > NOW() "); $login_status = (mysql_num_rows($qid) > 0); $app->logMsg(sprintf('Returning %s login status for user_id %s', ($login_status ? 'true' : 'false'), $user_id), LOG_DEBUG, __FILE__, __LINE__); return $login_status; } // User login test need only be run once per script execution. We cache the result in the session. if ($this->_authentication_tested && isset($_SESSION['_auth_sql'][$this->_ns]['authenticated'])) { $app->logMsg(sprintf('Returning cached authentication status: %s', ($_SESSION['_auth_sql'][$this->_ns]['authenticated'] ? 'true' : 'false')), LOG_DEBUG, __FILE__, __LINE__); return $_SESSION['_auth_sql'][$this->_ns]['authenticated']; } // Tesing login should occur once. This is the first time. Set flag. $this->_authentication_tested = true; // 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, or their usernames to the list of match_remote_ip_exempt_usernames. if ($trusted_net = ipInRange(getRemoteAddr(), $this->_params['trusted_networks'])) { $user_in_trusted_network = true; $app->logMsg(sprintf('User_id %s accessing from trusted network %s', ($this->get('user_id') ? $this->get('user_id') . ' (' . $this->get('username') . ')' : 'unknown'), $trusted_net ), LOG_DEBUG, __FILE__, __LINE__); } else if (preg_match('/proxy.aol.com$/i', getRemoteAddr(true))) { $user_in_trusted_network = true; $app->logMsg(sprintf('User_id %s accessing from trusted network proxy.aol.com', ($this->get('user_id') ? $this->get('user_id') . ' (' . $this->get('username') . ')' : 'unknown') ), LOG_DEBUG, __FILE__, __LINE__); } else { $user_in_trusted_network = false; } // Do we match the user's remote IP at all? Yes, if set in config and not disabled for specific user. if ($this->getParam('match_remote_ip') && !$this->get('match_remote_ip_exempt')) { $remote_ip_is_matched = (isset($_SESSION['_auth_sql'][$this->_ns]['remote_ip']) && $_SESSION['_auth_sql'][$this->_ns]['remote_ip'] == getRemoteAddr()) || $user_in_trusted_network; } else { $app->logMsg(sprintf('User_id %s exempt from remote_ip match (comparing %s == %s)', ($this->get('user_id') ? $this->get('user_id') . ' (' . $this->get('username') . ')' : 'unknown'), $_SESSION['_auth_sql'][$this->_ns]['remote_ip'], getRemoteAddr() ), LOG_DEBUG, __FILE__, __LINE__); $remote_ip_is_matched = true; } // Test login with information stored in session. Skip IP matching for users from trusted networks. if (isset($_SESSION['_auth_sql'][$this->_ns]['authenticated']) && true === $_SESSION['_auth_sql'][$this->_ns]['authenticated'] && isset($_SESSION['_auth_sql'][$this->_ns]['username']) && !empty($_SESSION['_auth_sql'][$this->_ns]['username']) && isset($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) && strtotime($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) > time() - $this->_params['login_timeout'] && isset($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) && strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) > time() - $this->_params['idle_timeout'] && $remote_ip_is_matched ) { // User is authenticated! $_SESSION['_auth_sql'][$this->_ns]['last_access_datetime'] = date('Y-m-d H:i:s'); // Update the DB with the last_access_datetime and increment the seconds_online. $db->query(" UPDATE " . $this->_params['db_table'] . " SET seconds_online = seconds_online + (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)) + 1, last_access_datetime = '" . $this->get('last_access_datetime') . "' WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "' "); if (mysql_affected_rows($db->getDBH()) > 0) { // 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. return true; } else { $app->logMsg(sprintf('Session update failed; record not found for user_id %s (%s).', $this->get('user_id'), $this->get('username')), LOG_NOTICE, __FILE__, __LINE__); } } else if (isset($_SESSION['_auth_sql'][$this->_ns]['authenticated']) && true === $_SESSION['_auth_sql'][$this->_ns]['authenticated']) { // User is authenticated, but login has expired. if (strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) > time() - 43200) { // Only raise message if last session is less than 12 hours old. $app->raiseMsg(_("Your session has expired. You need to log-in again."), MSG_NOTICE, __FILE__, __LINE__); } // Log the reason for login expiration. $expire_reasons = array(); if (empty($_SESSION['_auth_sql'][$this->_ns]['username'])) { $expire_reasons[] = 'username not found'; } if (strtotime($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) <= time() - $this->_params['login_timeout']) { $expire_reasons[] = sprintf('login_timeout expired (%s older than %s seconds ago)', $_SESSION['_auth_sql'][$this->_ns]['login_datetime'], $this->_params['login_timeout']); } if (strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) <= time() - $this->_params['idle_timeout']) { $expire_reasons[] = sprintf('idle_timeout expired (%s older than %s seconds ago)', $_SESSION['_auth_sql'][$this->_ns]['last_access_datetime'], $this->_params['idle_timeout']); } if ($_SESSION['_auth_sql'][$this->_ns]['remote_ip'] != getRemoteAddr()) { if ($this->getParam('match_remote_ip') && !$this->get('match_remote_ip_exempt') && !$user_in_trusted_network) { // There are three cases when a remote IP match will be the cause of a session termination: // 1. match_remote_ip config is enabled // 2. user is not match_remote_ip_exempt (set in the user_data, or in the match_remote_ip_exempt_usernames list) // 3. the user is connecting from a trusted network (their IP is listed in the trusted_networks or from *.proxy.aol.com) $expire_reasons[] = sprintf('remote_ip not matched (%s != %s)', $_SESSION['_auth_sql'][$this->_ns]['remote_ip'], getRemoteAddr()); } else { $expire_reasons[] = sprintf('remote_ip not matched but user was exempt from this check (%s != %s)', $_SESSION['_auth_sql'][$this->_ns]['remote_ip'], getRemoteAddr()); } } $app->logMsg(sprintf('User_id %s (%s) session expired: %s', $this->get('user_id'), $this->get('username'), join(', ', $expire_reasons)), LOG_INFO, __FILE__, __LINE__); } else { $app->logMsg(sprintf('Session not authenticated: %s', getDump($_SESSION['_auth_sql'][$this->_ns])), LOG_DEBUG, __FILE__, __LINE__); } // User is not authenticated. $this->clear(); return false; } /** * Redirect user to login page if they are not logged in. * * @param string $message The text description of a message to raise. * @param int $type The type of message: MSG_NOTICE, * MSG_SUCCESS, MSG_WARNING, or MSG_ERR. * @param string $file __FILE__. * @param string $line __LINE__. * @access public */ function requireLogin($message='', $type=MSG_NOTICE, $file=null, $line=null) { $app =& App::getInstance(); if (!$this->isLoggedIn()) { // Display message for requiring login. (RaiseMsg will ignore empty strings.) if ('' != $message) { $app->raiseMsg($message, $type, $file, $line); } // Login scripts must have the same 'login' tag for boomerangURL verification/manipulation. $app->setBoomerangURL(absoluteMe(), 'login'); $app->dieURL($this->_params['login_url']); } } /** * This sets the 'blocked' field for a user in the db_table, and also * adds an optional reason * * @param string $reason The reason for blocking the account. */ function blockAccount($user_id=null, $reason='') { $app =& App::getInstance(); $db =& DB::getInstance(); $this->initDB(); if ($this->getParam('blocking')) { if (mb_strlen($db->escapeString($reason)) > 255) { // blocked_reason field is varchar(255). $app->logMsg(sprintf('Blocked reason provided is greater than 255 characters: %s', $reason), LOG_WARNING, __FILE__, __LINE__); } // Get user_id if specified. $user_id = isset($user_id) ? $user_id : $this->get('user_id'); $db->query(" UPDATE " . $this->_params['db_table'] . " SET blocked = 'true', blocked_reason = '" . $db->escapeString($reason) . "' WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "' "); } } /** * Tests if the "blocked" flag is set for a user. * * @param int $user_id User id to look for. * @return boolean True if the user is blocked, false otherwise. */ function isBlocked($user_id=null) { $db =& DB::getInstance(); $this->initDB(); if ($this->getParam('blocking')) { // Get user_id if specified. $user_id = isset($user_id) ? $user_id : $this->getVal('user_id'); $qid = $db->query(" SELECT 1 FROM " . $this->_params['db_table'] . " WHERE blocked = 'true' AND " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "' "); return mysql_num_rows($qid) === 1; } } /** * Unblocks a user in the db_table, and clears any blocked_reason. */ function unblockAccount($user_id=null) { $db =& DB::getInstance(); $this->initDB(); if ($this->getParam('blocking')) { // Get user_id if specified. $user_id = isset($user_id) ? $user_id : $this->get('user_id'); $db->query(" UPDATE " . $this->_params['db_table'] . " SET blocked = '', blocked_reason = '' WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "' "); } } /** * Returns true if username already exists in database. * * @param string $username Username to look for. * @return bool True if username exists. */ function usernameExists($username) { $db =& DB::getInstance(); $this->initDB(); $qid = $db->query(" SELECT 1 FROM " . $this->_params['db_table'] . " WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "' "); return (mysql_num_rows($qid) > 0); } /** * Returns a username for a specified user id. * * @param string $user_id User id to look for. * @return string Username, or false if none found. */ function getUsername($user_id) { $db =& DB::getInstance(); $this->initDB(); $qid = $db->query(" SELECT " . $this->_params['db_username_column'] . " FROM " . $this->_params['db_table'] . " WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "' "); if (list($username) = mysql_fetch_row($qid)) { return $username; } else { return false; } } /** * Returns a randomly generated password based on $pattern. The pattern is any * sequence of 'x', 'V', 'C', 'v', 'c', or 'd' and if it is something like 'cvccv' this * function will generate a pronounceable password. Recommend using more complex * patterns, at minimum the US State Department standard: cvcddcvc. * * - x a random upper or lower alpha character or digit * - C a random upper or lower consonant * - V a random upper or lower vowel * - c a random lowercase consonant * - v a random lowercase vowel * - d a random digit * * @param string $pattern a sequence of character types, above. * @return string a password */ function generatePassword($pattern='CvcdCvc') { $app =& App::getInstance(); if (preg_match('/[^xCVcvd]/', $pattern)) { $app->logMsg(sprintf('Invalid pattern: %s', $pattern), LOG_WARNING, __FILE__, __LINE__); $pattern='CvcdCvc'; } $str = ''; for ($i=0; $i_params['encryption_type']) { case AUTH_ENCRYPT_PLAINTEXT : return $password; break; case AUTH_ENCRYPT_CRYPT : // If comparing plaintext password with a hash, provide first two chars of the hash as the salt. return isset($salt) ? crypt($password, mb_substr($salt, 0, 2)) : crypt($password); break; case AUTH_ENCRYPT_SHA1 : return sha1($password); break; case AUTH_ENCRYPT_SHA1_HARDENED : $hash = sha1($app->getParam('signing_key') . $password . $more_salt); // Increase key strength by 12 bits. for ($i=0; $i < 4096; $i++) { $hash = sha1($hash); } return $hash; break; case AUTH_ENCRYPT_MD5 : return md5($password); break; case AUTH_ENCRYPT_MD5_HARDENED : // Include salt to improve hash $hash = md5($app->getParam('signing_key') . $password . $more_salt); // Increase key strength by 12 bits. for ($i=0; $i < 4096; $i++) { $hash = md5($hash); } return $hash; break; default : $app->logMsg(sprintf('Authentication encrypt type specified is unrecognized: %s', $this->_params['encryption_type']), LOG_NOTICE, __FILE__, __LINE__); return false; break; } } /** * */ function setPassword($user_id=null, $password) { $app =& App::getInstance(); $db =& DB::getInstance(); $this->initDB(); // Get user_id if specified. $user_id = isset($user_id) ? $user_id : $this->get('user_id'); // Get old password. $qid = $db->query(" SELECT userpass FROM " . $this->_params['db_table'] . " WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "' "); if (!list($old_encrypted_password) = mysql_fetch_row($qid)) { $app->logMsg(sprintf('Cannot set password for nonexistent user_id %s', $user_id), LOG_NOTICE, __FILE__, __LINE__); return false; } // Compare old with new to ensure we're actually *changing* the password. $encrypted_password = $this->encryptPassword($password); if ($old_encrypted_password == $encrypted_password) { $app->logMsg(sprintf('Not setting password: new is the same as old.', null), LOG_INFO, __FILE__, __LINE__); return false; } // Issue the password change query. $db->query(" UPDATE " . $this->_params['db_table'] . " SET userpass = '" . $db->escapeString($encrypted_password) . "' WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "' "); if (mysql_affected_rows($db->getDBH()) != 1) { $app->logMsg(sprintf('Failed to update password for user_id %s', $user_id), LOG_WARNING, __FILE__, __LINE__); return false; } return true; } /** * Resets the password for the user with the specified id. * * @param string $user_id The id of the user to reset. * @param string $reason Additional message to add to the reset email. * @return string The user's new password. */ function resetPassword($user_id=null, $reason='') { $app =& App::getInstance(); $db =& DB::getInstance(); $this->initDB(); // Get user_id if specified. $user_id = isset($user_id) ? $user_id : $this->get('user_id'); // Reset password of a specific user. $qid = $db->query(" SELECT * FROM " . $this->_params['db_table'] . " WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "' "); if (!$user_data = mysql_fetch_assoc($qid)) { $app->logMsg(sprintf('Reset password failed. User_id %s not found.', $user_id), LOG_NOTICE, __FILE__, __LINE__); return false; } // Get new password. $password = $this->generatePassword(); // Update password query. $this->setPassword($user_id, $password); // Make sure user has an email on record before continuing. if (!isset($user_data['email']) || '' == trim($user_data['email'])) { $app->logMsg(sprintf('Password reset but notification failed, no email address for user_id %s (%s).', $user_data[$this->_params['db_primary_key']], $user_data[$this->_params['db_username_column']]), LOG_NOTICE, __FILE__, __LINE__); } else { // Send the new password in an email. $email = new Email(array( 'to' => $user_data['email'], 'from' => sprintf('%s <%s>', $app->getParam('site_name'), $app->getParam('site_email')), 'subject' => sprintf('%s password change', $app->getParam('site_name')) )); $email->setTemplate('codebase/services/templates/email_reset_password.txt'); $email->replace(array( 'SITE_NAME' => $app->getParam('site_name'), 'SITE_URL' => $app->getParam('site_url'), 'SITE_EMAIL' => $app->getParam('site_email'), 'NAME' => ('' != $user_data['first_name'] . $user_data['last_name'] ? $user_data['first_name'] . ' ' . $user_data['last_name'] : $user_data[$this->_params['db_username_column']]), 'USERNAME' => $user_data[$this->_params['db_username_column']], 'PASSWORD' => $password, 'REASON' => ('' == trim($reason) ? '' : trim($reason) . ' '), // Add a space after the reason if it exists for better formatting. )); $email->send(); } return array( 'username' => $user_data[$this->_params['db_username_column']], 'userpass' => $password ); } /** * If the current user has access to the specified $security_zone, return true. * If the optional $user_type is supplied, test that against the zone. * * NOTE: "user_type" used to be called "priv" in some older implementations. * * @param constant $security_zone string of comma delimited privileges for the zone * @param string $user_type a privilege that might be found in a zone * @return bool true if user is a member of security zone, false otherwise */ function inClearanceZone($security_zone, $user_type='') { // return true; /// WTF? $zone_members = preg_split('/,\s*/', $security_zone); $user_type = empty($user_type) ? $this->get('user_type') : $user_type; // If the current user's privilege level is NOT in that array or if the // user has no privilege, return false. Otherwise the user is clear. if (!in_array($user_type, $zone_members) || empty($user_type)) { return false; } else { return true; } } /** * This function tests a list of arguments $security_zone against the priv that the current user has. * If the user doesn't have one of the supplied privs, die. * * NOTE: "user_type" used to be called "priv" in some older implementations. * * @param constant $security_zone string of comma delimited privileges for the zone */ function requireAccessClearance($security_zone, $message='') { $app =& App::getInstance(); // return true; /// WTF? $zone_members = preg_split('/,\s*/', $security_zone); /* If the current user's privilege level is NOT in that array or if the * user has no privilege, DIE with a message. */ if (!in_array($this->get('user_type'), $zone_members) || !$this->get('user_type')) { $message = empty($message) ? _("You have insufficient privileges to view that page.") : $message; $app->raiseMsg($message, MSG_NOTICE, __FILE__, __LINE__); $app->dieBoomerangURL(); } } } // end class // CIDR cheat-sheet // // Netmask Netmask (binary) CIDR Notes // _____________________________________________________________________________ // 255.255.255.255 11111111.11111111.11111111.11111111 /32 Host (single addr) // 255.255.255.254 11111111.11111111.11111111.11111110 /31 Unusable // 255.255.255.252 11111111.11111111.11111111.11111100 /30 2 useable // 255.255.255.248 11111111.11111111.11111111.11111000 /29 6 useable // 255.255.255.240 11111111.11111111.11111111.11110000 /28 14 useable // 255.255.255.224 11111111.11111111.11111111.11100000 /27 30 useable // 255.255.255.192 11111111.11111111.11111111.11000000 /26 62 useable // 255.255.255.128 11111111.11111111.11111111.10000000 /25 126 useable // 255.255.255.0 11111111.11111111.11111111.00000000 /24 "Class C" 254 useable // // 255.255.254.0 11111111.11111111.11111110.00000000 /23 2 Class C's // 255.255.252.0 11111111.11111111.11111100.00000000 /22 4 Class C's // 255.255.248.0 11111111.11111111.11111000.00000000 /21 8 Class C's // 255.255.240.0 11111111.11111111.11110000.00000000 /20 16 Class C's // 255.255.224.0 11111111.11111111.11100000.00000000 /19 32 Class C's // 255.255.192.0 11111111.11111111.11000000.00000000 /18 64 Class C's // 255.255.128.0 11111111.11111111.10000000.00000000 /17 128 Class C's // 255.255.0.0 11111111.11111111.00000000.00000000 /16 "Class B" // // 255.254.0.0 11111111.11111110.00000000.00000000 /15 2 Class B's // 255.252.0.0 11111111.11111100.00000000.00000000 /14 4 Class B's // 255.248.0.0 11111111.11111000.00000000.00000000 /13 8 Class B's // 255.240.0.0 11111111.11110000.00000000.00000000 /12 16 Class B's // 255.224.0.0 11111111.11100000.00000000.00000000 /11 32 Class B's // 255.192.0.0 11111111.11000000.00000000.00000000 /10 64 Class B's // 255.128.0.0 11111111.10000000.00000000.00000000 /9 128 Class B's // 255.0.0.0 11111111.00000000.00000000.00000000 /8 "Class A" // // 254.0.0.0 11111110.00000000.00000000.00000000 /7 // 252.0.0.0 11111100.00000000.00000000.00000000 /6 // 248.0.0.0 11111000.00000000.00000000.00000000 /5 // 240.0.0.0 11110000.00000000.00000000.00000000 /4 // 224.0.0.0 11100000.00000000.00000000.00000000 /3 // 192.0.0.0 11000000.00000000.00000000.00000000 /2 // 128.0.0.0 10000000.00000000.00000000.00000000 /1 // 0.0.0.0 00000000.00000000.00000000.00000000 /0 IP space ?>