Enhance security of cookie-based login
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(); //Set the same login token in a Cookie $cookieToken = new CHttpCookie(self::LOGIN_TOKEN, $user->logintoken); $cookieToken->expire = time() + Yii::app()->params['rememberMeTime']; Yii::app()->request->cookies[self::LOGIN_TOKEN] = $cookieToken; 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(Yii::app()->request->cookies[UserIdentity::LOGIN_TOKEN])){ return false; } $user = User::model()->notsafe()->findbyPk($id); $cookieLoginToken = Yii::app()->request->cookies[UserIdentity::LOGIN_TOKEN]->value; if(isset($cookieLoginToken, $user) && $cookieLoginToken == $user->logintoken) { return true; } return false; } }