PHP Authentication Class

This code is a beta release of the authentication class I've been developing for some time. Expect this page to be fairly dynamic while I complete it and work out the last of the 'features'.

It's now easily extendable and I've included an extended include file as well as the main class file. Granted I still have some work to do on making the authentication functions themselves simple enough to extend without any challenges. (And then of course to document them.)

Please feel free to contact me if you're planning on trying it out. Use the contact form on the left. For the moment I'm more than happy to work with people on getting it running in a grander effort to ensure it's 100% ready.

phpauth.extend

Extending this class is just like others except you need to define some configuration parameters in the extending class. Below is an example.

<?

require 'phpauth.class';

Class 
Auth extends PHPAuth
{
        
// The following parameters should be set to match
        // your database and server.
        
public $session_name '***';
        protected 
$dbhost '***';
        protected 
$database '***';
        protected 
$dbuser '***';
        protected 
$dbpass '***';
        protected 
$dbusertable '***';

    public 
$reqlogin 0;
    public 
$retainrequest 0;
    public 
$promptlogin 'http://www.domain.dom/loginform.html';
    public 
$postlogin '';
    public 
$postlogout 'http://www.domain.dom';
    public 
$postfailure 'http://www.domain.dom/authfailed.html';
    public 
$accessdenied 'http://www.domain.dom/403.html';
    public 
$reqperm 0;
    protected 
$debug 0;


}

$auth = new Auth();
if(isset(
$postlogin) && is_string($postlogin)) $auth->postlogin $postlogin;
if(isset(
$retainrequest) && $retainrequest$auth->retainrequest 1;
$auth->setup_login();
$auth->showsetup();
$auth->start_login();

?>
phpauth.class
<?

/*
 *
 * Copyright (C) 2005  James Bly
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Version 0.91.1
 *
 * Todo (by priority):
 *   Make auth function calls easily extendable
 *   Fix timeouts
 *   Get rid of postgres error messages in dbstatus
 *   Check all calls under request() for possible injection points
 *   Add line number to all log messages
 *   Add comments to all functions/variables
 *   Configuration flag to enforce SSL auth
 *
 * WARNING: This code is beta and shouldn't be used for
 * production purposes. It also might have holes and security
 * issues as only a cursory review has been done prior to its
 * release. Use it at your own risk.
 *
 */

class PHPAuth
{

    
// Variable holders for the current page and session
    
protected $sessid '';
    protected 
$sess_timeout 20;
    protected 
$auth_timeout 20;
    public 
$reqlogin 0;
    public 
$retainrequest 0;
    public 
$promptlogin '';
    public 
$postlogin '';
    public 
$postlogout '';
    public 
$postfailure '';
    public 
$accessdenied '';
    public 
$reqperm 0;

    protected 
$timeout 1200;        // 20 minute (1200 second) timeout
    
private $phpauthaction '';        // login, logout, etc
    
private $instance 0;
    private 
$action '';
    private 
$requested '';

        
// The following parameters should be set to match
        // your database and server.
        
protected $session_name 'extend this class!';
        protected 
$dbhost 'extend this class!';
        protected 
$database 'extend this class!';
        protected 
$dbuser 'extend this class!';
        protected 
$dbpass 'extend this class!';
        protected 
$dbusertable 'extend this class!';


    protected 
$debug 0;            // added calls to self::logger if set to 2, just comment info if 1
    
private $special = array();        // For tracking special debug messages in showsetup()
    
private $logauth 1;

    private 
$conn null;            // holder for the database connection

    
private $authkey '';
    private 
$authuser '';
    private 
$authpass '';

    
// Variable holders for the server settings
    
private $method '';
    private 
$host '';
    private 
$file '';
    private 
$params = array();


    function 
__construct()
    {
        
$this->instance rand(1000,9999);
        
self::debug("__construct""start for instance %s"$this->instance);

        
self::get_vars();        // setup all the variables that auth needs
        
self::debug("__construct""received request for %s"$this->requested);

        if(!
self::checkrequired())    // check that required details are present
        
{
            
self::logger("checkrequired - requirements not met");
            exit(
"phpauth requirements not met");
        }

        
error_reporting(E_ALL);
    }

    public function 
setup_login()
    {

        
self::authsession();        // startup the session

        
if($this->reqlogin && !$this->getuser())
            
$this->action 'login';

    }

    public function 
start_login()
    {
        if(
strlen($this->action) > 0)
            switch(
$this->action)
            {
                case 
'login':
                    if(!
$this->authuser || !$this->authpass)
                    {
                        
self::debug("__construct""finish for instance %s, sending to promptlogin"$this->instance);
                        
self::redirect("promptlogin", array('redirect'=> urlencode($_SESSION['requested'])));
                        exit;
                    } else {
                        
self::dologin($this->authuser$this->authpass);
                        
self::debug("__construct""dologin returned %s for %s"$this->authkey$this->authuser);
                        if(!
$this->authkey || strlen($this->authkey) <= 0)
                        {
                            
self::debug("__construct""finish for instance %s, sending to postfailure"$this->instance);
                            
self::debug("__construct""-------------------------------------------------------");
                            
self::debug("__construct""authentication failed for user: %s"$this->authuser);
                            
self::redirect("postfailure");
                            exit;
                        }
                        else
                        {
                            
self::debug("__construct""setting up authenticated session");
                            
$_SESSION['authkey'] = $this->authkey;
                            
$_SESSION['authuser'] = $this->authuser;
                            
self::debug("__construct""finish for instance %s, sending to postlogin"$this->instance);
                            
self::debug("__construct""-------------------------------------------------------");

                            if(
self::is_key("redirect"$_SESSION) && $_SESSION['redirect'])
                            {
                                
self::debug("__construct""postlogin redirecting to session redirect");
                                
$redir self::request_session("redirect");
                                
session_unregister("redirect");
                                
self::redirect($redir);
                            } else { 
                                
self::debug("__construct""postlogin redirecting to supplied redirect");
                                
self::redirect($this->postlogin); 
                            }
                            exit;
                        }
                    }
                    break;
                case 
'logout':
                    
self::dologout($this->authuser);
                    
self::debug("__construct""construct finish for instance %s, logout completed"$this->instance);
                    
self::debug("__construct""-------------------------------------------------------");
                    exit;
                    break;
                default:
                    
self::debug("__construct""Unknown authentication: %s"$this->action);
                    
self::debug("__construct""construct finish for instance %s, action request failure"$this->instance);
                    
self::debug("__construct""-------------------------------------------------------");
                    exit(
"Unknown authentication: " $this->action);
                    break;
            }

        if(
self::is_key("authkey"$_SESSION) && self::is_key("authuser"$_SESSION))
        {
            if(
self::validateauth($_SESSION['authuser'], $_SESSION['authkey']))
            {
                
self::updateauth($_SESSION['authuser'], $_SESSION['authkey']);
                if(
self::getperm($_SESSION['authuser']) < $this->reqperm)
                {
                    
self::debug("__construct""%s failed required permissions level for %s"$this->authuser$this->file);
                    
self::debug("__construct""construct finish for instance %s, sending to accessdenied"$this->instance);
                    
self::debug("__construct""-------------------------------------------------------");
                    if(
$this->accessdeniedself::redirect("accessdenied");
                    else 
self::redirect(sprintf("%s://%s"$this->method$this->host));
                    exit;
                }
            }
            else
            {
                
self::debug("__construct""construct finish for instance %s, forcing logout"$this->instance);
                
self::dologout($_SESSION['authuser']);
            }
        }
        
self::debug("__construct""construct finish for instance %s, reached end"$this->instance);
        
self::debug("__construct""-------------------------------------------------------");
    }

    private function 
dologin($user$pass$case 0)
    {
        switch(
$case)
        {
            case 
'1';
                break;
            case 
'2':
                
$user strtolower($user);
                
$pass strtolower($pass);
                break;
            default:
                
$user strtolower($user);    
                break;
        }

        
self::dbconnect();

        if(
self::dbstatus())
        {
            
$sql sprintf("select id,lower(userid),password from %s where lower(userid) = '%s'"$this->dbusertable$user);
            
self::debug("dologin""calling dbquery(%s)"$sql);
            
$data self::dbquery($sql);
            
self::dbclose();
            
$x count($data);
            if(
$x 1)
            {
                
self::logger(sprintf("dologin error: dbquery returned more than one match for %s"$user));
                return 
0;
            } 
            else if (
$x == 0)
            {
                
self::logger(sprintf("dologin error: dbquery cannot find user %s"$user));
                return 
0;
            }

            
$dbpass $data[0]['password'];
            if(
preg_match("/(md5|sha1)?:?(.*)/"$dbpass$matches))
            {
                if(
$matches[1] == ''$matches[1] = 'md5';
                
$hashmeth $matches[1];
                
$truehash $matches[2];
            }

            
$hashpass self::mkhashpass($pass$hashmeth);
            if(
$hashpass == $truehash)
            {
                
self::debug("dologin""authentication successful for %s"$user);
                
$this->authkey self::genauthkey();
                
$sql sprintf("update %s set authkey = '%s', timeexp = %d, lastauth = %d where lower(userid) = '%s'"
                    
$this->dbusertable$this->authkeytime() + $this->timeouttime(), $user);
                
self::debug("dologin""calling dbquery(%s)"$sql);
                
self::dbquery($sql);
                
self::debug("dologin""setting authkey to %s"$this->authkey);
                if(
$this->logauthself::logger(sprintf("authentication successful for %s"$user));
                return 
$this->authkey;
            }

            return 
0;
        }
            
        
    }

    private function 
dologout()
    {
        if(
self::is_key("authkey"$_SESSION) && self::is_key("authuser"$_SESSION))
        {
            
self::dbconnect();
            
$sql sprintf("update %s set authkey = '' where lower(userid) = '%s'"$this->dbusertable$_SESSION['authuser']);
            
self::debug("dologout""calling dbquery(%s)"$sql);
            
self::dbquery($sql);
            
self::dbclose();
            if(
$this->logauthself::logger(sprintf("authentication logout for %s"$_SESSION['authuser']));
            
session_unregister('authkey');
            
session_unregister('authuser');
        }
        if(
$this->postlogoutself::redirect("postlogout");
        else 
self::redirect(sprintf("%s://%s"$this->method$this->host));
    }

    public function 
getperm($userid 0)
    {
        if(!
$userid$userid self::getuser();
        
$sql sprintf("select permissions from %s where lower(userid) = '%s'"$this->dbusertable$userid);
        
self::debug("getperm""calling dbquery(%s)"$sql);
        
$data self::dbquery($sql);
        if(
$data 1) return -1;
        else return 
$data[0]["permissions"];
    }


    private function 
updateauth($userid$authkey)
    {
        
$sql sprintf("update %s set timeexp = %d, authkey = '%s' where lower(userid) = '%s'"$this->dbusertable, (time() + $this->timeout), $authkey$userid);
        
self::debug("updateauth""calling dbquery(%s)"$sql);
        
self::dbquery($sql);
    }

    private function 
validateauth($userid$authkey)
    {
        
$opendb 0;
        if(!
self::dbstatus())
            
$opendb 1;

        if(
$opendbself::dbconnect();
        
$sql sprintf("select id from %s where lower(userid) = '%s' and authkey = '%s'"$this->dbusertable$userid$authkey);
        
self::debug("validateauth""calling dbquery(%s)"$sql);
        
$data self::dbquery($sql);
        if(
count($data) < 1) return 0;
        
self::updateauth($userid$authkey);
        if(
$opendbself::dbclose();
        return 
1;
    }

    public function 
getuser()
     {
        if(
self::is_key("authuser"$_SESSION))
            return 
$_SESSION['authuser'];
        else
            return 
null;
    }

    private function 
dbconnect()
    {
        
$constr sprintf("host=%s port=5432 dbname=%s user=%s password=%s"$this->dbhost$this->database$this->dbuser$this->dbpass);

        if(!
self::dbstatus()) $this->conn pg_connect($constr);
        if(!
self::dbstatus()) self::logger("dbconnect: connection failed");
    }

    private function 
dbclose()
    {
        if(
self::dbstatus())
            
pg_close($this->conn);
    }

    private function 
dbstatus()
    {
        
error_reporting(E_ALL E_WARNING);
        if(
pg_connection_status($this->conn) === PGSQL_CONNECTION_OK)
        {
            
self::debug("dbstatus""connection ok");
            
error_reporting(E_ALL);
            return 
1;
        }
        else
        {
            
self::debug("dbstatus""connection failed");
            
error_reporting(E_ALL);
            return 
0;
        }
    }

    private function 
dbquery($sql)
    {
        
$data = array();

        
$opendb 0;
        if(!
self::dbstatus())
            
$opendb 1;

        if(
$opendbself::dbconnect();

        if(!
self::dbstatus())
        {
            
self::debug("dbquery""can't find connection");
            return 
null;
        }

        
$result pg_query($this->conn$sql);
        
self::debug(sprintf("dbquery""check status: %s"pg_connection_status($this->conn)));
        
$data pg_fetch_all($result);
        if(
$opendbself::dbclose();
        return 
$data;
    }

    public function 
get_settings()
    {
        return array(
'promptlogin' => $this->promptlogin
                     
'postlogin' => $this->postlogin,
                     
'postfailure' => $this->postfailure
                 
'accessdenied' => $this->accessdenied,
                 
'reqperm' => $this->reqperm
                 
'phpauthaction' => $this->phpauthaction);
    }


    private function 
redirect($where$vars = array())
    {
        
$split 1;
        
self::debug("redirect""sending to %s with vars:\n%s"$wherevar_export($vars1));
        if(!
$where)
        {
            
self::debug("redirect""received null where value");
            exit;
        }
        if(
self::is_prop($where))
        {
            if(
strstr($this->$where"?")) $split 0;
            
$qs self::query_string($vars, array(), $split);
            
self::debug(sprintf("redirect""is_prop: sending %s%s"$this->$where$qs));
            
header("Location: " $this->$where $qs);
        } else {
            if(
strstr($where"?")) $split 0;
            
$qs self::query_string($vars, array(), $split);
            
self::debug(sprintf("redirect""not is_prop: sending %s%s"$where$qs));
            
header(sprintf("Location: %s"$where $qs));
        }
    }

    private function 
is_prop($what)
    {
        if(
in_array($what, array('promptlogin''postlogin''postlogout''postfailure'
                                 
'accessdenied''reqperm''phpauthaction''instance'
                     
'reqlogin''action')))
            return 
true;
        return 
false;
    }

    private function 
rebuildrequest($uri$params = array())
    {
        
$uri .= self::query_string($params, array("phpauth"));
        return 
$uri;
    }

    private function 
query_string($vars = array(), $filter = array(), $split 1)
    {
        
$uri '';
        
$final = array();
        
$x count($vars);
        if(
$x == 0) return $uri;

        if(
$split$uri .= '?';
        else 
$uri .= '&';

        foreach(
array_keys($vars) as $p)
        {
            if(
in_array($p$filter)) 
            {
                
self::debug("query_string""filtering out %s"$p);
                continue;
            }
            
$final[$p] = $vars[$p];
        }

        
$uri .= http_build_query($final);
        
self::debug("query_string""built %s"$uri);
        return 
$uri;
    }

    public function 
mkhashpass($password$hash 'md5'$header 0)
    {
        switch(
$hash)
        {
            case 
'md5': return sprintf("%s%s", ($header)?'md5:':''bin2hex(mhash(MHASH_MD5$password)));
            case 
'sha1': return sprintf("%s%s", ($header)?'sha1:':''bin2hex(mhash(MHASH_SHA1$password)));
        }

    }

    private function 
request($item$len 0$regex '')
    {
            
$data '';
    
            if(isset(
$_GET[$item])) $data = ($len)?substr($_GET[$item], 0$len):$_GET[$item];
            if(isset(
$_POST[$item])) $data = ($len)?substr($_POST[$item], 0$len):$_POST[$item];
    
            if(
strlen($regex) && !preg_match($regex$data)) return FALSE;
            if(
$data) return $data;
            return 
FALSE;
    }

    private function 
request_session($item$len 0$regex '')
    {
            
$data '';
    
            if(isset(
$_SESSION[$item])) $data = ($len)?substr($_SESSION[$item], 0$len):$_SESSION[$item];
    
            if(
strlen($regex) && !preg_match($regex$data)) return FALSE;
            if(
$data) return $data;
            return 
FALSE;
    }


    private function 
checkrequired()
    {
        return 
1;        // alls well that ends well - holder
    
}

    public function 
showsetup($return 0)
    {
        
$settings = array();
        
$settings['special'] = $this->special;
        
$settings['get_settings'] = self::get_settings();
        
$settings['reqlogin'] = $this->reqlogin;
        
$settings['retainrequest'] = $this->retainrequest;
        
$settings['reqperm'] = $this->reqperm;
        
$settings['timeout'] = $this->timeout;
        
$settings['phpauthaction'] = $this->phpauthaction;
        
$settings['action'] = $this->action;
        
$settings['requested'] = $this->requested;
        
$settings['session_name'] = $this->session_name;
        
$settings['dbhost'] = $this->dbhost;
        
$settings['database'] = $this->database;
        
$settings['dbuser'] = $this->dbuser;
        
$settings['dbpass'] = $this->dbpass;
        
$settings['dbusertable'] = $this->dbusertable;
        
$settings['authkey'] = $this->authkey;
        
$settings['authuser'] = $this->authuser;
        
$settings['authpass'] = $this->authpass;
        
$settings['SESSION'] = $_SESSION;
        
$settings['COOKIE'] = $_COOKIE;
        
$settings['SERVER'] = $_SERVER;

        if(
$return) return $settings;
        else 
printf("<!--\n%s\n-->"var_export($settings1));
    }


    private function 
get_vars()
    {
        
$qs = array();
        if(
$_SERVER['SERVER_PORT'] == 443)
            
$this->method="https";
        else
            
$this->method="http";
        
$this->host $_SERVER['SERVER_NAME'];
        
$this->file preg_replace("/(.*)\?.*/""$1"$_SERVER['REQUEST_URI']);
        
parse_str($_SERVER['QUERY_STRING'], $qs);
        
self::debug("get_vars""parse_str provides: %s for %s"var_export($qs1), $_SERVER['QUERY_STRING']);
        

        
$this->requested sprintf("%s://%s%s%s"$this->method$this->host$this->fileself::query_string($qs, array("phpauth""as"), 1));
        
self::debug("get_vars""set requested: %s"$this->requested);

        
$this->authuser strtolower(self::request("authuser"60'/^[A-Za-z0-9\@\.\-]*$/'));
        
$this->authpass self::request("authpass"40);
    }

    private function 
authsession()
    {

        if(
is_array($_COOKIE) && in_array('sessid'array_keys($_COOKIE)))
             
$this->sessid $_COOKIE['sessid'];

        if(
strlen($this->sessid) < 32)
        {
            
$this->sessid self::genauthkey();
            
setcookie("sessid"$this->sessid);
            
session_id($this->sessid);
            
session_set_cookie_params(60*$this->sess_timeout'/'$this->host0);
        }

        
session_name($this->session_name);
        
session_start();
        
$_SESSION['sessid'] = $this->sessid;
        
$_SESSION['testkey'] = self::genauthkey();
        
$_SESSION['requested'] = $this->requested;
        
$this->action self::request("phpauth"30'/^[A-Za-z]*$/');

        if(
self::is_key("authkey"$_SESSION))
            
$this->authkey self::request_session("authkey"40'/^[A-Za-z0-9]*$/');

        if(
self::is_key("authuser"$_SESSION))
            
$this->authuser self::request_session("authuser"40'/^[A-Za-z0-9\@\.\-]*$/');

        
self::debug("authsession""set SESSION to: " var_export($_SESSION1));

        if(!
$this->retainrequest && self::is_key('requested'$_SESSION) && $_SESSION['requested'])
        {
            
$_SESSION['redirect'] = self::request_session("requested");
        }
        else if(!
$this->retainrequest && $this->postlogin)
        {
            
$_SESSION['redirect'] = $this->postlogin;
        }


    }

    public function 
genauthkey()
    {
        
$ip $_SERVER['REMOTE_ADDR'];
        
$seed rand(100000000,999999999);
        return 
bin2hex(mhash(MHASH_MD5$ip $seed));
    }

    public function 
is_key($needle ''$haystack = array())
    {
            if(
is_array($haystack) && strlen($needle) > && count($haystack) > 0)
                    if(
in_array($needlearray_keys($haystack))) return true;
            else return 
false;
            else
                    return 
false;
    }


    public function 
plogger($message)
    {
        
self::logger("public $message");
    }

    private function 
logger($message)
    {
        
//syslog(LOG_NOTICE, sprintf("phpauth[%s]: %s", $this->instance, $message));
        //file_put_contents("/tmp/authout.log", sprintf("phpauth[%s]: %s\n", $this->instance, $message), FILE_APPEND);
        
array_push($this->special$message);
    }

    private function 
debug()
    {
            if(
$this->debug 2) return;
            
$a func_get_args();
            
$fn sprintf("%s():"array_shift($a));
            
$f array_shift($a);
            
self::logger(vsprintf(sprintf("%-20s%s"$fn$f), $a));
    }

}



?>