Skip to content
Snippets Groups Projects
class.auth.php 23.4 KiB
Newer Older
  • Learn to ignore specific revisions
  • Jared Hancock's avatar
    Jared Hancock committed
    <?php
    require(INCLUDE_DIR.'class.ostsession.php');
    require(INCLUDE_DIR.'class.usersession.php');
    
    
    abstract class AuthenticatedUser {
        //Authorization key returned by the backend used to authorize the user
        private $authkey;
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        // Get basic information
    
        abstract function getId();
        abstract function getUsername();
        abstract function getRole();
    
    
        //Backend used to authenticate the user
        abstract function getAuthBackend();
    
        //Authentication key
    
        function setAuthKey($key) {
            $this->authkey = $key;
        }
    
        function getAuthKey() {
            return $this->authkey;
        }
    
    
        // logOut the user
        function logOut() {
    
            if ($bk = $this->getAuthBackend())
                return $bk->signOut($this);
    
            return false;
        }
    
    interface AuthDirectorySearch {
        /**
         * Indicates if the backend can be used to search for user information.
         * Lookup is performed to find user information based on a unique
         * identifier.
         */
        function lookup($id);
    
        /**
         * Indicates if the backend supports searching for usernames. This is
         * distinct from information lookup in that lookup is intended to lookup
         * information based on a unique identifier
         */
        function search($query);
    }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
    /**
     * Authentication backend
     *
     * Authentication provides the basis of abstracting the link between the
     * login page with a username and password and the staff member,
     * administrator, or client using the system.
     *
     * The system works by allowing the AUTH_BACKENDS setting from
     * ost-config.php to determine the list of authentication backends or
     * providers and also specify the order they should be evaluated in.
     *
     * The authentication backend should define a authenticate() method which
     * receives a username and optional password. If the authentication
     * succeeds, an instance deriving from <User> should be returned.
     */
    
    abstract class AuthenticationBackend {
        static protected $registry = array();
    
    Jared Hancock's avatar
    Jared Hancock committed
        static $name;
        static $id;
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        /* static */
        static function register($class) {
    
            if (is_string($class) && class_exists($class))
    
    Jared Hancock's avatar
    Jared Hancock committed
                $class = new $class();
    
    
            if (!is_object($class)
                    || !($class instanceof AuthenticationBackend))
                return false;
    
            return static::_register($class);
        }
    
        static function _register($class) {
    
            // XXX: Raise error if $class::id is already in the registry
            static::$registry[$class::$id] = $class;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        static function allRegistered() {
            return static::$registry;
        }
    
    
        static function getBackend($id) {
    
    
            if ($id
                    && ($backends = static::allRegistered())
                    && isset($backends[$id]))
                return $backends[$id];
    
        static function process($username, $password=null, &$errors) {
    
    
            if (!$username)
                return false;
    
    
            $backends =  static::getAllowedBackends($username);
            foreach (static::allRegistered() as $bk) {
                if ($backends //Allowed backends
                        && $bk->supportsAuthentication()
    
                        && !in_array($bk::$id, $backends))
    
    Jared Hancock's avatar
    Jared Hancock committed
                    // User cannot be authenticated against this backend
                    continue;
    
                // All backends are queried here, even if they don't support
                // authentication so that extensions like lockouts and audits
                // can be supported.
    
    Jared Hancock's avatar
    Jared Hancock committed
                $result = $bk->authenticate($username, $password);
    
    
                if ($result instanceof AuthenticatedUser
    
                        && ($bk->login($result, $bk)))
    
    Jared Hancock's avatar
    Jared Hancock committed
                    return $result;
                // TODO: Handle permission denied, for instance
                elseif ($result instanceof AccessDenied) {
                    $errors['err'] = $result->reason;
                    break;
                }
            }
    
    Jared Hancock's avatar
    Jared Hancock committed
            $info = array('username'=>$username, 'password'=>$password);
            Signal::send('auth.login.failed', null, $info);
        }
    
    
        function processSignOn(&$errors) {
    
            foreach (static::allRegistered() as $bk) {
    
                // All backends are queried here, even if they don't support
                // authentication so that extensions like lockouts and audits
                // can be supported.
                $result = $bk->signOn();
                if ($result instanceof AuthenticatedUser) {
    
                    //Perform further Object specific checks and the actual login
    
                    if (!$bk->login($result, $bk))
    
                    return $result;
                }
                // TODO: Handle permission denied, for instance
                elseif ($result instanceof AccessDenied) {
                    $errors['err'] = $result->reason;
                    break;
                }
            }
        }
    
    
        static function searchUsers($query) {
            $users = array();
    
            foreach (static::allRegistered() as $bk) {
    
                if ($bk instanceof AuthDirectorySearch) {
                    $users += $bk->search($query);
                }
            }
            return $users;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        /**
         * Fetches the friendly name of the backend
         */
        function getName() {
            return static::$name;
        }
    
        /**
         * Indicates if the backed supports authentication. Useful if the
         * backend is used for logging or lockout only
         */
        function supportsAuthentication() {
            return true;
        }
    
        /**
         * Indicates if the backend supports changing a user's password. This
         * would be done in two fashions. Either the currently-logged in user
         * want to change its own password or a user requests to have their
         * password reset. This requires an administrative privilege which this
         * backend might not possess, so it's defined in supportsPasswordReset()
         */
        function supportsPasswordChange() {
            return false;
        }
    
        function supportsPasswordReset() {
            return false;
        }
    
    
        function signOn() {
    
            return null;
    
        protected function validate($auth) {
            return null;
        }
    
    
        abstract function authenticate($username, $password);
        abstract function login($user, $bk);
    
        abstract static function getUser(); //Validates  authenticated users.
    
        abstract function getAllowedBackends($userid);
    
        abstract protected function getAuthKey($user);
    
        abstract static function signOut($user);
    
    Jared Hancock's avatar
    Jared Hancock committed
    }
    
    class RemoteAuthenticationBackend {
        var $create_unknown_user = false;
    }
    
    
    abstract class StaffAuthenticationBackend  extends AuthenticationBackend {
    
        static private $_registry = array();
    
        static function _register($class) {
            static::$_registry[$class::$id] = $class;
        }
    
        static function allRegistered() {
            return array_merge(self::$_registry, parent::allRegistered());
        }
    
        function isBackendAllowed($staff, $bk) {
    
            if (!($backends=self::getAllowedBackends($staff->getId())))
                return true;  //No restrictions
    
            return in_array($bk::$id, array_map('strtolower', $backends));
        }
    
        function getAllowedBackends($userid) {
    
            $backends =array();
            //XXX: Only one backend can be specified at the moment.
            $sql = 'SELECT backend FROM '.STAFF_TABLE
                  .' WHERE backend IS NOT NULL ';
            if (is_numeric($userid))
                $sql.= ' AND staff_id='.db_input($userid);
            else {
                $sql.= ' AND (username='.db_input($userid) .' OR email='.db_input($userid).')';
            }
    
            if (($res=db_query($sql)) && db_num_rows($res))
                $backends[] = db_result($res);
    
            return array_filter($backends);
        }
    
    
        function login($staff, $bk) {
    
            if (!$bk || !($staff instanceof Staff))
    
                return false;
    
            // Ensure staff is allowed for realz to be authenticated via the backend.
    
            if (!static::isBackendAllowed($staff, $bk)
                || !($authkey=$bk->getAuthKey($staff)))
    
                return false;
    
            //Log debug info.
            $ost->logDebug('Staff login',
    
                sprintf("%s logged in [%s], via %s", $staff->getUserName(),
    
                    $_SERVER['REMOTE_ADDR'], get_class($bk))); //Debug.
    
            $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() '
    
                .' WHERE staff_id='.db_input($staff->getId());
    
            db_query($sql);
    
    
            //Tag the authkey.
            $authkey = $bk::$id.':'.$authkey;
    
    
            //Now set session crap and lets roll baby!
    
            $authsession = &$_SESSION['_auth']['staff'];
    
            $authsession = array(); //clear.
            $authsession['id'] = $staff->getId();
            $authsession['key'] =  $authkey;
    
            $staff->setAuthKey($authkey);
            $staff->refreshSession(); //set the hash.
    
            $_SESSION['TZ_OFFSET'] = $staff->getTZoffset();
            $_SESSION['TZ_DST'] = $staff->observeDaylight();
    
    
            //Regenerate session id.
            $sid = session_id(); //Current id
            session_regenerate_id(true);
            // Destroy old session ID - needed for PHP version < 5.1.0
            // DELME: remove when we move to php 5.3 as min. requirement.
            if(($session=$ost->getSession()) && is_object($session)
                    && $sid!=session_id())
                $session->destroy($sid);
    
    
            Signal::send('auth.login.succeeded', $staff);
    
            $staff->cancelResetTokens();
    
        /* Base signOut
         *
         * Backend should extend the signout and perform any additional signout
         * it requires.
         */
    
    
        static function signOut($staff) {
            global $ost;
    
            $_SESSION['_auth']['staff'] = array();
            $ost->logDebug('Staff logout',
                    sprintf("%s logged out [%s]",
                        $staff->getUserName(),
                        $_SERVER['REMOTE_ADDR'])); //Debug.
    
    
        // Called to get authenticated user (if any)
    
        static function getUser() {
    
            if (!isset($_SESSION['_auth']['staff'])
                    || !$_SESSION['_auth']['staff']['key'])
                return null;
    
            list($id, $auth) = explode(':', $_SESSION['_auth']['staff']['key']);
    
            if (!($bk=static::getBackend($id)) //get the backend
                    || !$bk->supportsAuthentication() //Make sure it can authenticate
                    || !($staff = $bk->validate($auth)) //Get AuthicatedUser
                    || !($staff instanceof Staff)
                    || $staff->getId() != $_SESSION['_auth']['staff']['id'] // check ID
                    )
                return null;
    
            $staff->setAuthKey($_SESSION['_auth']['staff']['key']);
    
    
            return $staff;
        }
    
    
        function authenticate($username, $password) {
            return false;
        }
    
    
        // Generic authentication key for staff's backend is the username
    
        protected function getAuthKey($staff) {
    
    
            if(!($staff instanceof Staff))
                return null;
    
            return $staff->getUsername();
        }
    
        protected function validate($authkey) {
    
            if (($staff = new StaffSession($authkey)) && $staff->getId())
                return $staff;
    
    }
    
    abstract class UserAuthenticationBackend  extends AuthenticationBackend {
    
        static private $_registry = array();
    
        static function _register($class) {
            static::$_registry[$class::$id] = $class;
        }
    
        static function allRegistered() {
            return array_merge(self::$_registry, parent::allRegistered());
        }
    
        function getAllowedBackends($userid) {
            // White listing backends for specific user not supported.
            return array();
        }
    
        function login($user, $bk) {
            global $ost;
    
    
            if (!$user || !$bk
                    || !$bk::$id //Must have ID
                    || !($authkey = $bk->getAuthKey($user)))
    
            //Tag the authkey.
            $authkey = $bk::$id.':'.$authkey;
    
            //Set the session goodies
    
            $authsession = &$_SESSION['_auth']['user'];
    
            $authsession = array(); //clear.
            $authsession['id'] = $user->getId();
            $authsession['key'] = $authkey;
    
            $_SESSION['TZ_OFFSET'] = $ost->getConfig()->getTZoffset();
            $_SESSION['TZ_DST'] = $ost->getConfig()->observeDaylightSaving();
    
    
            //The backend used decides the format of the auth key.
            // XXX: encrypt to hide the bk??
            $user->setAuthKey($authkey);
    
    
            $user->refreshSession(); //set the hash.
    
            //Log login info...
    
            $msg=sprintf('%s (%s) logged in [%s]',
                    $user->getUserName(), $user->getId(), $_SERVER['REMOTE_ADDR']);
    
            $ost->logDebug('User login', $msg);
    
            //Regenerate session ID.
            $sid=session_id(); //Current session id.
            session_regenerate_id(TRUE); //get new ID.
            if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id())
                $session->destroy($sid);
    
            return true;
        }
    
    
        function authenticate($username, $password) {
            return false;
        }
    
    
        static function signOut($user) {
            global $ost;
    
            $_SESSION['_auth']['user'] = array();
            $ost->logDebug('User logout',
                    sprintf("%s logged out [%s]",
                        $user->getUserName(), $_SERVER['REMOTE_ADDR']));
        }
    
    
        protected function getAuthKey($user) {
    
        static function getUser() {
    
            if (!isset($_SESSION['_auth']['user'])
                    || !$_SESSION['_auth']['user']['key'])
                return null;
    
            list($id, $auth) = explode(':', $_SESSION['_auth']['user']['key']);
    
            if (!($bk=static::getBackend($id)) //get the backend
                    || !$bk->supportsAuthentication() //Make sure it can authenticate
                    || !($user=$bk->validate($auth)) //Get AuthicatedUser
                    || !($user instanceof AuthenticatedUser) // Make sure it user
                    || $user->getId() != $_SESSION['_auth']['user']['id'] // check ID
                    )
                return null;
    
            $user->setAuthKey($_SESSION['_auth']['user']['key']);
    
            return $user;
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    /**
     * This will be an exception in later versions of PHP
     */
    class AccessDenied {
        function AccessDenied() {
            call_user_func_array(array($this, '__construct'), func_get_args());
        }
        function __construct($reason) {
            $this->reason = $reason;
        }
    }
    
    /**
     * Simple authentication backend which will lock the login form after a
     * configurable number of attempts
     */
    
    abstract class AuthStrikeBackend extends AuthenticationBackend {
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function authenticate($username, $password=null) {
    
            return static::authStrike($username, $password);
        }
    
        function signOn() {
            return static::authStrike('Unknown');
        }
    
    
        static function signOut($user) {
            return false;
        }
    
    
    
        function login($user, $bk) {
            return false;
        }
    
    
        function supportsAuthentication() {
            return false;
        }
    
        function getAllowedBackends($userid) {
            return array();
        }
    
    
        function getAuthKey($user) {
            return null;
        }
    
    
        abstract function  authStrike($username, $password=null);
    }
    
    /*
     * Backend to monitor staff's failed login attempts
     */
    class StaffAuthStrikeBackend extends  AuthStrikeBackend {
    
        function authstrike($username, $password=null) {
            global $ost;
    
            $cfg = $ost->getConfig();
    
            $authsession = &$_SESSION['_auth']['staff'];
    
            if($authsession['laststrike']) {
                if((time()-$authsession['laststrike'])<$cfg->getStaffLoginTimeout()) {
                    $authsession['laststrike'] = time(); //reset timer.
    
    Jared Hancock's avatar
    Jared Hancock committed
                    return new AccessDenied('Max. failed login attempts reached');
                } else { //Timeout is over.
                    //Reset the counter for next round of attempts after the timeout.
    
                    $authsession['laststrike']=null;
                    $authsession['strikes']=0;
    
            $authsession['strikes']+=1;
            if($authsession['strikes']>$cfg->getStaffMaxLogins()) {
                $authsession['laststrike']=time();
    
    Jared Hancock's avatar
    Jared Hancock committed
                $alert='Excessive login attempts by a staff member?'."\n".
                       'Username: '.$username."\n"
                       .'IP: '.$_SERVER['REMOTE_ADDR']."\n"
                       .'TIME: '.date('M j, Y, g:i a T')."\n\n"
    
                       .'Attempts #'.$authsession['strikes']."\n"
    
    Jared Hancock's avatar
    Jared Hancock committed
                       .'Timeout: '.($cfg->getStaffLoginTimeout()/60)." minutes \n\n";
                $ost->logWarning('Excessive login attempts ('.$username.')', $alert,
                        $cfg->alertONLoginError());
                return new AccessDenied('Forgot your login info? Contact Admin.');
            //Log every other failed login attempt as a warning.
    
            } elseif($authsession['strikes']%2==0) {
    
    Jared Hancock's avatar
    Jared Hancock committed
                $alert='Username: '.$username."\n"
                        .'IP: '.$_SERVER['REMOTE_ADDR']."\n"
                        .'TIME: '.date('M j, Y, g:i a T')."\n\n"
    
                        .'Attempts #'.$authsession['strikes'];
    
    Jared Hancock's avatar
    Jared Hancock committed
                $ost->logWarning('Failed staff login attempt ('.$username.')', $alert, false);
            }
        }
    
    }
    StaffAuthenticationBackend::register(StaffAuthStrikeBackend);
    
    /*
     * Backend to monitor user's failed login attempts
     */
    class UserAuthStrikeBackend extends  AuthStrikeBackend {
    
        function authstrike($username, $password=null) {
            global $ost;
    
            $cfg = $ost->getConfig();
    
    
            $authsession = &$_SESSION['_auth']['user'];
    
    
            //Check time for last max failed login attempt strike.
    
            if($authsession['laststrike']) {
                if((time()-$authsession['laststrike'])<$cfg->getClientLoginTimeout()) {
                    $authsession['laststrike'] = time(); //renew the strike.
    
                    return new AccessDenied('You\'ve reached maximum failed login attempts allowed.');
                } else { //Timeout is over.
                    //Reset the counter for next round of attempts after the timeout.
    
                    $authsession['laststrike'] = null;
                    $authsession['strikes'] = 0;
    
            $authsession['strikes']+=1;
            if($authsession['strikes']>$cfg->getClientMaxLogins()) {
                $authsession['laststrike'] = time();
    
                $alert='Excessive login attempts by a user.'."\n".
                        'Login: '.$username.': '.$password."\n".
                        'IP: '.$_SERVER['REMOTE_ADDR']."\n".'Time:'.date('M j, Y, g:i a T')."\n\n".
    
                        'Attempts #'.$authsession['strikes'];
    
                $ost->logError('Excessive login attempts (user)', $alert, ($cfg->alertONLoginError()));
                return new AccessDenied('Access Denied');
    
            } elseif($authsession['strikes']%2==0) { //Log every other failed login attempt as a warning.
    
                $alert='Login: '.$username.': '.$password."\n".'IP: '.$_SERVER['REMOTE_ADDR'].
    
                       "\n".'TIME: '.date('M j, Y, g:i a T')."\n\n".'Attempts #'.$authsession['strikes'];
    
                $ost->logWarning('Failed login attempt (user)', $alert);
            }
    
    UserAuthenticationBackend::register(UserAuthStrikeBackend);
    
    
    class osTicketAuthentication extends StaffAuthenticationBackend {
        static $name = "Local Authentication";
    
    Jared Hancock's avatar
    Jared Hancock committed
        static $id = "local";
    
        function authenticate($username, $password) {
            if (($user = new StaffSession($username)) && $user->getId() &&
                    $user->check_passwd($password)) {
    
                //update last login && password reset stuff.
                $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() ';
                if($user->isPasswdResetDue() && !$user->isAdmin())
                    $sql.=',change_passwd=1';
                $sql.=' WHERE staff_id='.db_input($user->getId());
                db_query($sql);
    
                return $user;
            }
        }
    
    StaffAuthenticationBackend::register(osTicketAuthentication);
    
    class PasswordResetTokenBackend extends StaffAuthenticationBackend {
        static $id = "pwreset.staff";
    
    
        function supportsAuthentication() {
            return false;
        }
    
    
        function signOn($errors=array()) {
            if (!isset($_POST['userid']) || !isset($_POST['token']))
                return false;
            elseif (!($_config = new Config('pwreset')))
                return false;
            elseif (($staff = new StaffSession($_POST['userid'])) &&
                    !$staff->getId())
                $errors['msg'] = 'Invalid user-id given';
            elseif (!($id = $_config->get($_POST['token']))
                    || $id != $staff->getId())
                $errors['msg'] = 'Invalid reset token';
            elseif (!($ts = $_config->lastModified($_POST['token']))
                    && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
                $errors['msg'] = 'Invalid reset token';
            elseif (!$staff->forcePasswdRest())
                $errors['msg'] = 'Unable to reset password';
            else
                return $staff;
        }
    
        function login($staff, $bk) {
            $_SESSION['_staff']['reset-token'] = $_POST['token'];
            Signal::send('auth.pwreset.login', $staff);
            return parent::login($staff, $bk);
        }
    }
    StaffAuthenticationBackend::register(PasswordResetTokenBackend);
    
    
    /*
     * AuthToken Authentication Backend
     *
     * Provides auto-login facility for end users with valid link
     *
     * Ticket used to loggin is tracked durring the session this is
     * important in the future when auto-logins will be
     * limited to single ticket view.
     */
    
    class AuthTokenAuthentication extends UserAuthenticationBackend {
        static $name = "Auth Token Authentication";
        static $id = "authtoken";
    
    
        function signOn() {
    
    
            if ($_GET['auth']) {
                if (($u = TicketUser::lookupByToken($_GET['auth'])))
                    $user = new ClientSession($u);
            }
    
            // Support old ticket based tokens.
            elseif ($_GET['t'] && $_GET['e'] && $_GET['a']) {
    
                if (($ticket = Ticket::lookupByNumber($_GET['t'], $_GET['e']))
    
                        // Using old ticket auth code algo - hardcoded here because it
                        // will be removed in ticket class in the upcoming rewrite
                        && !strcasecmp($_GET['a'], md5($ticket->getId() .  $_GET['e'] . SECRET_SALT))
    
                        && ($owner = $ticket->getOwner()))
                    $user = new ClientSession($owner);
    
        protected function getAuthKey($user) {
    
    
            if (!$this->supportsAuthentication() || !$user)
    
                return null;
    
            //Generate authkey based the type of ticket user
            // It's required to validate users going forward.
            $authkey = sprintf('%s%dt%dh%s',  //XXX: Placeholder
    
                        ($user->isOwner() ? 'o':'c'),
    
                        $user->getId(),
    
                        $user->getTicketId(),
                        md5($user->getId().$this->id));
    
            return $authkey;
    
        protected function validate($authkey) {
    
            $regex = '/^(?P<type>\w{1})(?P<id>\d+)t(?P<tid>\d+)h(?P<hash>.*)$/i';
            $matches = array();
            if (!preg_match($regex, $authkey, $matches))
                return false;
    
            $user = null;
            switch ($matches['type']) {
                case 'c': //Collaborator
    
                    $criteria = array( 'userId' => $matches['id'],
                            'ticketId' => $matches['tid']);
                    if (($c = Collaborator::lookup($criteria))
    
                            && ($c->getTicketId() == $matches['tid']))
                        $user = new ClientSession($c);
                    break;
                case 'o': //Ticket owner
                    if (($ticket = Ticket::lookup($matches['tid']))
    
                            && ($o = $ticket->getOwner())
                            && ($o->getId() == $matches['id']))
                        $user = new ClientSession($o);
    
            //Make sure the authkey matches.
            if (!$user || strcmp($this->getAuthKey($user), $authkey))
    
        }
    
    }
    UserAuthenticationBackend::register(AuthTokenAuthentication);
    
    
    Jared Hancock's avatar
    Jared Hancock committed
    ?>