Enhance security of cookie-based login

You are viewing revision #6 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version.

next (#7) »

Enhance security of cookie-based login

  1. What the Yii guide is saying
  2. Implementation

What the Yii guide is saying

When talking about cookie-base login the Yii guide indicates the following:

In addition, for any serious Web applications, we recommend using the following strategy to enhance the security of cookie-based login.

When a user successfully logs in by filling out a login form, we generate and store a random key in both the cookie state and in persistent storage on server side (e.g. database). Upon a subsequent request, when the user authentication is being done via the cookie information, we compare the two copies of this random key and ensure a match before logging in the user. If the user logs in via the login form again, the key needs to be re-generated. By using the above strategy, we eliminate the possibility that a user may re-use an old state cookie which may contain outdated state information.

To implement the above strategy, we need to override the following two methods:

  • CUserIdentity::authenticate(): this is where the real authentication is performed. If the user is authenticated, we should re-generate a new random key, and store it in the database as well as in the identity states via CBaseUserIdentity::setState.
  • CWebUser::beforeLogin(): this is called when a user is being logged in. We should check if the key obtained from the state cookie is the same as the one from the database.

In this tutorial I'll try to show how to implement this.

Implementation

The database

First we are going to add a logintoken field in the user table in the database.

ALTER TABLE user ADD logintoken VARCHAR(255);
The UserIdentity Component

We are going to modify the authenticate method by setting the login token, both in the db and a cookie

const LOGIN_TOKEN="logintoken";

//some more code

public function authenticate()
	{
		$user=User::model()->find('LOWER(username)=?',array(strtolower($this->username)));
		if($user===null)
			$this->errorCode=self::ERROR_USERNAME_INVALID;
		else if(!$user->validatePassword($this->password))
			$this->errorCode=self::ERROR_PASSWORD_INVALID;
		else
		{
			$this->_id=$user->id;
			$this->username=$user->username;
			$this->errorCode=self::ERROR_NONE;
		}
		
		// Generate a login token and save it in the DB
		$user->logintoken = sha1(uniqid(mt_rand(), true));
		$user->save();

                //the login token is saved as a state
		$this->setState(self::LOGIN_TOKEN, $user->logintoken);
        
		return $this->errorCode==self::ERROR_NONE;
	}

For the sake of this tutorial I used sha1(uniqid(mt_rand(), true)) to generate the token, but in real world applications I strongly advise you to use something more robust. There is a great library for generating random numbers and strings created by Anthony Ferrara that you could use: RandomLib.

In my configuration file, in the params section I have a rememberMeTime key holding the time a user may be cookie-logged, in seconds.

The WebUser component

Then we are going to extend the CWebUser component to check if the cookie value matches the DB in the beforeLogin method.

class WebUser extends CWebUser
{
	
	protected function beforeLogin($id,$states,$fromCookie)
	{
		//If the login is not cookie-based then there is no point to check
		if(!$fromCookie) {
			return true;
		}

		//The cookie isn't here, we refuse the login
		if(!isset($states[UserIdentity::LOGIN_TOKEN])){
			return false;
		}

		$user = User::model()->notsafe()->findbyPk($id);
		$cookieLogintoken = $states[UserIdentity::LOGIN_TOKEN];
		if(isset($cookieLoginToken, $user) 
                  && $cookieLoginToken == $user->logintoken) {
		    return true;
		}
		return false;
	}
}