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();
//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.
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;
}
}
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.