How to avoid Identity Theft in Zend Framework with Zend Auth

The following tutorial will show you how you can improve your overall security in your PHP application (the tutorial is based on Zend Framework 1.10 and on the use of Zend_Auth but you can easily adapt it to something for your own needs). You will learn how to extend the Zend_Auth class into a highly customizable & secure Project Authentication class

As I am building my applications, I always try to improve the code I write in some way. Today I thought about the security issues of any PHP application that uses an authenticating system.

Web application security issues

The major issues with security within web application are the following:

  • Cross-site Scripting (see Wikipedia – Cross-site scripting )
  • Unvalidated parameters
  • Broken access control
  • Error-handling problems
  • Insecure use of cryptography
  • Web and application server misconfiguration
  • Broken account and session management

While all are major issues, there is one particular issue that bugged me for some time. The Identity theft – Broken account and session management issue.

Why can one so easily still my session id cookie and suddenly gain access to my account in one particular web application? I know it its rather impossible to make this 100% hack-proof but I strongly believe that the system should be improved as much as possible.

In the following few lines, I will show you how you can just do that.

Our goal

Our goal is to implement a Zend Auth extension that adds a new level of security to the previously mentioned class.

This extension – let’s call it Project_Application_Auth – would check the Zend Auth storage for the IP and/or User Agent.

In order to do so, these should be set in the login process in the storage.

If the IP is different then the initial IP from the login process and / or the User Agent is not the same as the initial User Agent from the login process, then our extension would tell us that it is not a secure identity (aka it is safe to assume it has been stolen) and thus we should disconnect the user.

Reasons to use these methods

I came up with this idea since I asked myself: Is there really a case where i would actually end up with the same session ID but different IP or different browser?

The answer is: yes. There is a chance with the first-one-mentioned. If i have a dynamic IP, this could change without me knowing, which would result in me being unauthenticated.

But the fact is, this happens rarely (even if you IP changes, it won’t change more then once per week, right?).

You might ask why should the extension that I am about to show you, check the IP also when we can just check the User Agent (since this definitely cannot be the same on a different IP since the session id cookie would exist in only one browser, not any other ).

The answer is: because any hacker that stole your identity might just use the same browser as you did and thus bypass our checks completely.

And as a final reason, yes, if the hacker has the same IP as yours, steals your identity (session cookie id) and uses the same browser, then he will bypass the system. But hey, at least we can make it harder for him!

The Project Application Auth extension

The following class should go into your /path/to/the/project/……./library/Project/Application/Auth.php

class Project_Application_Auth extends Zend_Auth
{
    /**
     * Singleton instance
     *
     * @var Project_Application_Auth
     */
    protected static $_instance = null;

    /**
     * Defines how much time to wait until
     * to reinitialize the session id
     */
    protected static $_session_exp_time = 5;

    /**
     * Defines if to validate a secure identity
     */
    protected static $_secure = TRUE;

    /**
     * Defines secure identity check level
     *
     * 1 - Check only IP
     * 2 - Check only UserAgent
     * 3 - Check IP & UserAgent
     */
    protected static $_secure_level = 3;

    /**
     * Returns an instance of Project_Application_Auth
     *
     * Singleton pattern implementation
     *
     * @return Project_Application_Auth Provides a fluent interface
     */
    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }

    /**
     * Sets wheter the method @see hasSecureIdentity
     * to work or not. If set to FALSE then this extension will work
     * as the normal Zend_Auth class
     *
     * @param boolean true
     */
    public function setSecure($status = TRUE)
    {
    	if ($status === TRUE)
    	{
    		self::$_secure = TRUE;
    	}

    	if ($status === FALSE)
    	{
    		self::$_secure = FALSE;
    	}

    	return TRUE;
    }

    /**
     * Sets the level of security for the @see hasSecureIdentity method
     *
     * @param integer $level (can be 1 or 2 or 3)
     */
	public function setSecureLevel($level)
    {
    	if (in_array($level, array(1, 2, 3)))
    	{
    		self::$_secure_level = $level;
    	}

    	return TRUE;
    }

    /**
     * Returns true if and only if an identity is not stolen
     *
     * Checks if IP and/or User Agent (@see $_secure_level)
     * match the initial authentication data
     *
     * @return boolean
     */
    public function hasSecureIdentity()
    {
    	if (self::$_secure === FALSE)
    	{
    		return TRUE;
    	}

    	if (FALSE == $this->getStorage()->isEmpty())
    	{
	    	$storage = $this->getStorage()->read();

	    	if (self::$_secure_level == 3)
	    	{
		    	return $storage->ip == $_SERVER['REMOTE_ADDR']
		    		&& $storage->user_agent == $_SERVER['HTTP_USER_AGENT'];
	    	}
	    	elseif (self::$_secure_level == 2)
	    	{
	    		return $storage->user_agent == $_SERVER['HTTP_USER_AGENT'];
	    	}
	    	elseif (self::$_secure_level == 1)
	    	{
	    		return $storage->ip == $_SERVER['REMOTE_ADDR'];
	    	}
	    	else
	    	{
	    		return FALSE;
	    	}
    	}
    	else
    	{
    		return FALSE;
    	}
    }

    /**
     * If the Zend Auth Storage
     * has been initialized and is a Session Storage
     * and the last time it has been reinitialized is bigger then
     * the @see $session_exp_time then reinitialize the session id
     *
     * @return boolean
     */
    public function reinitSecurity()
    {
    	if (isset($_SESSION['Zend_Auth']))
    	{
	   		$zend_auth_session_namespace = new Zend_Session_Namespace('Zend_Auth');
			if (!isset($zend_auth_session_namespace->initialized)
				|| $zend_auth_session_namespace->initialized + self::$session_exp_time < time() ) {
				Zend_Session::regenerateId();
				$zend_auth_session_namespace->initialized = time();
			}
    	}

    	return TRUE;
    }
}

Now, let’s stop for a second and understand what all these spooky lines do:
First of all, the extension implements the singleton pattern. Then, we got 3 properties:

The first one -is_secure – allows us to turn on and off the whole class, leaving us with the usual Zend Auth system.

The second one, secure level, defines what to check for: either just IP or just User Agent or both.

The third one, session_exp_time defines when to reinitialize the Zend Auth Session Storage Id (aka PHPSESSID id) if the method reinitSecurity is called.

How to use the class

In your base controller – let’s call it Project_Application_Controller (which all controllers should extend) – you should have the following code :

abstract Class Project_Application_Controller extends Zend_Controller_Action
    /**
     * Initialize the application controller
     */
	public function init()
	{
		parent::init();
		$this->_initSession();
		debug($_SESSION);
	}

	/**
	 * Inits the User Session
	 * and refresh the session id
	 */
	protected function _initSession()
	{
		Project_Application_Auth::getInstance()->reinitSecurity();
	}

This basically just allows us to run the method reinitSecurity with each run of the application, while still allowing us to initialize the whole Front Controller.

Now, in your preDispatch() method you might have something like this:

if ( FALSE === Project_Application_Auth::getInstance()->hasIdentity() { //do stuff here } else { //other stuff here }

This basically just checks if the user has been authenticated previously. Exactly underneath this, add the following code:

if ( FALSE === Project_Application_Auth::getInstance()->hasSecureIdentity()
    && 'users' !== $this->getRequest()->getControllerName()
    && 'login' !== $this->getRequest()->getActionName()
    && 'error' !== $this->getRequest()->getControllerName())
{
    Zend_Auth::getInstance()->clearIdentity();
    // redirect to login page
}

I am assuming that you have a controller named users with an action login into it (change as you wish).

Lastly, in your in your login process, wherever you are running your Zend_Auth authenticate() method, add the following code :

$auth = Project_Application_Auth::getInstance();

$result = $auth->authenticate($authAdapter);

if ($result->isValid())
{
    $storage = $authAdapter->getResultRowObject();
    $storage->ip = $_SERVER['REMOTE_ADDR'];
    $storage->user_agent = $_SERVER['HTTP_USER_AGENT'];

    $storage = $auth->getStorage()->write($storage);
    return TRUE;
}
else
{
    return FALSE;
}

This will put your user object in your Zend Auth Storage along side the IP and User Agent.

Conclusion

Hope you liked it. I am very interested in hearing your thoughts about this system so you’re invited to leave your comments below and I will respond as quick as possible.

See you!