* Copyright 2001-2012 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 .
*/
/*
* Auth_File.inc.php
*
* The Auth_File class provides a htpasswd file implementation for
* authentication.
*
* @author Quinn Comendant
* @version 1.2
*/
// Usage example:
// $auth = new Auth_File();
// $auth->setParam(array(
// 'htpasswd_file' => COMMON_BASE . '/global/site_users.htpasswd',
// 'login_timeout' => 21600,
// 'idle_timeout' => 3600,
// 'login_url' => '/login.php'
// ));
class Auth_File
{
// Available encryption types for class Auth_File.
const ENCRYPT_MD5 = 'md5';
const ENCRYPT_CRYPT = 'crypt';
const ENCRYPT_SHA1 = 'sha1';
const ENCRYPT_PLAINTEXT = 'plaintext';
// Namespace of this auth object.
protected $_ns;
// Parameters to be specified by setParam().
protected $_params = array();
protected $_default_params = array(
// Full path to htpasswd file.
'htpasswd_file' => null,
// The type of encryption to use for passwords stored in the db_table. Use one of the self::ENCRYPT_* types specified above.
'encryption_type' => self::ENCRYPT_CRYPT,
// 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.
// This applies to admins and users. 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.
// This applies to admins and users. In seconds. 3600 seconds = 1 hour.
'idle_timeout' => 3600,
// 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(),
);
// Associative array of usernames to hashed passwords.
protected $_users = array();
/*
* Constructs a new htpasswd authentication object.
*
* @access public
*
* @param optional array $params A hash containing parameters.
*/
public function __construct($namespace='')
{
$this->_ns = $namespace;
// Initialize default parameters.
$this->setParam($this->_default_params);
}
/*
* 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
*/
public function setParam($params)
{
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.
*/
public function getParam($param)
{
$app = &App::getInstance();
if (array_key_exists($param, $this->_params)) {
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
*/
public function clear()
{
$app =& App::getInstance();
$_SESSION['_auth_file'][$this->_ns] = array('authenticated' => false);
$app->logMsg(sprintf('Cleared %s auth', $this->_ns), LOG_DEBUG, __FILE__, __LINE__);
}
/*
* 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.
*/
public function set($key, $val)
{
if (!isset($_SESSION['_auth_file'][$this->_ns]['user_data'])) {
$_SESSION['_auth_file'][$this->_ns]['user_data'] = array();
}
$_SESSION['_auth_file'][$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.
*/
public function get($key, $default='')
{
if (isset($_SESSION['_auth_file'][$this->_ns][$key])) {
return $_SESSION['_auth_file'][$this->_ns][$key];
} else if (isset($_SESSION['_auth_file'][$this->_ns]['user_data'][$key])) {
return $_SESSION['_auth_file'][$this->_ns]['user_data'][$key];
} else {
return $default;
}
}
/*
* Find out if a set of login credentials are valid. Only supports
* htpasswd files with DES passwords right now.
*
* @access public
*
* @param string $username The username to check.
* @param array $password The password to compare to username.
*
* @return boolean Whether or not the credentials are valid.
*/
public function authenticate($username, $password)
{
$app = &App::getInstance();
if ('' == trim($password)) {
$app->logMsg(_("No password provided for authentication."), LOG_INFO, __FILE__, __LINE__);
return false;
}
// Load users file.
$this->_loadHTPasswdFile();
if (!isset($this->_users[$username])) {
$app->logMsg(_("User ID provided does not exist."), LOG_INFO, __FILE__, __LINE__);
return false;
}
if ($this->_encrypt($password, $this->_users[$username]) != $this->_users[$username]) {
$app->logMsg(sprintf('Authentication failed for user %s', $username), LOG_INFO, __FILE__, __LINE__);
return false;
}
// Authentication successful!
return true;
}
/*
* If user passes authentication create authenticated session.
*
* @access public
*
* @param string $username The username to check.
* @param array $password The password to compare to username.
*
* @return boolean Whether or not the credentials are valid.
*/
public function login($username, $password)
{
$username = mb_strtolower(trim($username));
$this->clear();
if (!$this->authenticate($username, $password)) {
// No login: failed authentication!
return false;
}
$_SESSION['_auth_file'][$this->_ns] = array(
'authenticated' => true,
'username' => $username,
'login_datetime' => date('Y-m-d H:i:s'),
'last_access_datetime' => date('Y-m-d H:i:s'),
'remote_ip' => getRemoteAddr()
);
// 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.
*
* @access public
*/
public function isLoggedIn()
{
$app = &App::getInstance();
// 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.
if ($trusted_net = ipInRange(getRemoteAddr(), $this->_params['trusted_networks'])) {
$user_in_trusted_network = true;
$app->logMsg(sprintf('User %s accessing from trusted network %s', $_SESSION['_auth_file'][$this->_ns]['username'], $trusted_net), LOG_DEBUG, __FILE__, __LINE__);
} else {
$user_in_trusted_network = false;
}
// Test login with information stored in session. Skip IP matching for users from trusted networks.
if (isset($_SESSION['_auth_file'][$this->_ns])
&& true === $_SESSION['_auth_file'][$this->_ns]['authenticated']
&& !empty($_SESSION['_auth_file'][$this->_ns]['username'])
&& strtotime($_SESSION['_auth_file'][$this->_ns]['login_datetime']) > time() - $this->_params['login_timeout']
&& strtotime($_SESSION['_auth_file'][$this->_ns]['last_access_datetime']) > time() - $this->_params['idle_timeout']
&& ($_SESSION['_auth_file'][$this->_ns]['remote_ip'] == getRemoteAddr() || $user_in_trusted_network)
) {
// User is authenticated!
$_SESSION['_auth_file'][$this->_ns]['last_access_datetime'] = date('Y-m-d H:i:s');
return true;
} else if (isset($_SESSION['_auth_file'][$this->_ns]) && true === $_SESSION['_auth_file'][$this->_ns]['authenticated']) {
if (strtotime($_SESSION['_auth_file'][$this->_ns]['last_access_datetime']) > time() - 43200) {
// Only raise message if last session is less than 12 hours old.
$app->raiseMsg(_("Your session has closed. You need to log-in again."), MSG_NOTICE, __FILE__, __LINE__);
}
// Log the reason for login expiration.
$expire_reasons = array();
if (empty($_SESSION['_auth_file'][$this->_ns]['username'])) {
$expire_reasons[] = 'username not found';
}
if (strtotime($_SESSION['_auth_file'][$this->_ns]['login_datetime']) <= time() - $this->_params['login_timeout']) {
$expire_reasons[] = 'login_timeout expired';
}
if (strtotime($_SESSION['_auth_file'][$this->_ns]['last_access_datetime']) <= time() - $this->_params['idle_timeout']) {
$expire_reasons[] = 'idle_timeout expired';
}
if ($_SESSION['_auth_file'][$this->_ns]['remote_ip'] != getRemoteAddr() && !$user_in_trusted_network) {
$expire_reasons[] = sprintf('remote_ip not matched (%s != %s)', $_SESSION['_auth_file'][$this->_ns]['remote_ip'], getRemoteAddr());
}
$app->logMsg(sprintf('User %s session expired: %s', $_SESSION['_auth_file'][$this->_ns]['username'], join(', ', $expire_reasons)), LOG_INFO, __FILE__, __LINE__);
}
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
*/
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.)
$app->raiseMsg($message, $type, $file, $line);
// Login scripts must have the same 'login' tag for boomerangURL verification/manipulation.
$app->setBoomerangURL(getenv('REQUEST_URI'), 'login');
$app->dieURL($this->_params['login_url']);
}
}
/*
* Wrapper function for compatibility with lib/Lock.inc.php.
*
* @param string $username Username to return.
* @return string Username, or false if none found.
*/
public function getUsername($username) {
if ('' != $username) {
return $username;
} else {
return false;
}
}
/*
* Reads the configured htpasswd file into the _users array.
*
* @access public
* @return false on error, true on success.
* @author Quinn Comendant
* @version 1.0
* @since 18 Apr 2006 18:17:48
*/
protected function _loadHTPasswdFile()
{
$app = &App::getInstance();
static $users = null;
if (!file_exists($this->_params['htpasswd_file'])) {
$app->logMsg(sprintf('htpasswd file missing or not specified: %s', $this->_params['htpasswd_file']), LOG_ERR, __FILE__, __LINE__);
return false;
}
if (!isset($users)) {
if (false === ($users = file($this->_params['htpasswd_file']))) {
$app->logMsg(sprintf('Could not read htpasswd file: %s', $this->_params['htpasswd_file']), LOG_ERR, __FILE__, __LINE__);
return false;
}
}
if (is_array($users)) {
foreach ($users as $u) {
list($user, $pass) = explode(':', $u, 2);
$this->_users[trim($user)] = trim($pass);
}
return true;
}
return false;
}
/*
* Hash a given password according to the configured encryption
* type.
*
* @param string $password The password to encrypt.
* @param string $encrypted_password The currently encrypted password to use as salt, if needed.
*
* @return string The hashed password.
*/
protected function _encrypt($password, $encrypted_password=null)
{
switch ($this->_params['encryption_type']) {
case self::ENCRYPT_PLAINTEXT :
return $password;
break;
case self::ENCRYPT_SHA1 :
return sha1($password);
break;
case self::ENCRYPT_MD5 :
return md5($password);
break;
case self::ENCRYPT_CRYPT :
default :
return crypt($password, $encrypted_password);
break;
}
}
} // end class