A few questions about the login process.

I have some difficulties understanding the way the login process needs to work .

First question is in the authenticate() method .

Let’s say i have something like:





    public function authenticate()

	{

		if($this->username && $this->hashedPassword are not valid records)

		{

            $this->errorCode = 'Invalid login credentials';

		}

		else

		{    

		    $row='ROW data from database';  

			$this->_id=$row->user_id;//getId();

            $this->_name=$row->username;//getName();

            $this->errorCode = self::ERROR_NONE ;

		}

		

		return ! $this->errorCode;

	}



Now, i know that $this->_id=‘Something’ and $this->_name=‘Something’ will be used by getId() and getName()

to set the state for later use in Yii::app()->user->

But, if i want to add more data here, what’s the correct approach? It is very confusing, because i can do something like :




$this->setState(key, value);//UserIdentity 

//OR

CWebUser::setState(key, value);//Yii::app()->user->setState(key, value);

//OR

Yii::app()->session->add();

//and i believe that even $_SESSION[key]=value; will work .






I do understand that using, $this->setState() will prefix the data with a private key and using Yii::app()->session->add() will not prefix the data with the private key .

So why is that? When should i use one and when should i use other ?

Now, have in mind the following situation:

I have a shopping cart (no i don’t, but just to show the idea) and i use the database to store the sessions and i have auto login set to true because i want my customers not to lose their cart contents .

If i use Yii::app()->user->setState() will be a problem because the data will be copied into the cookie also and the cookie size is limited. If i use Yii::app()->session i can store how much i want, but what’s happening if the user has 100 items in the cart, then he closes his browser and he comes back after a few days ? He is logged in automatically, but the cart content is gone right ? So how can i avoid this ?

Finally, i know that cookie auto login is not too safe, but also i saw in the user guide :

LINK HERE




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.




Has somebody tried this? If so, explaining a bit how to do it would be great because i really don’t get it.

Again, i am confused of phrases like :

When he says cookie state and persistent storage, what he means ?

I believe that something Yii::app()->user->setState(‘xyz’,‘secret token’), because a copy of the token is added to the cookie also, am i right ?

Another thing is :

Again, because i cannot understand the above lines, i can’t find a solution here.

I mean, yes, i extend CWebUser and overwrite beforeLogin and i use the new class




<?php


class MyCWebUser extends CWebUser{


    public function beforeLogin()

    {

        if($this->allowAutoLogin)

        {

            $cookieToken='from where i retrieve this?';

            $persistentToken='what about this ?';

            return $cookie===$persistent;

            

        }

        return true;

    }  


}



But further i have no clue, so any idea would be great at this point.

Thanks in advance :)

I partially share your confusion. But some of your assumptions are wrong, e.g. that Yii::app()->user->setState() will use cookies as storage. That’s only happening for CUserIdentity::setState().

See here for another thread, where we had a similar discussion:

http://www.yiiframework.com/forum/index.php?/topic/11858-security-implications-with-cwebuser/page__view__findpost__p__64403

Hi Mike and thanks for your reply, i really appreciate it .

Yes i might be wrong with some of assumptions, but that’s because the documentation confuses me in some places as described above.

I saw the thread you gave as a link, but is purely an open discussion which made me open this thread because i still have my confusions.

Do you have any idea to who i should write to get an answer?

Maybe the creator of the framework, or somebody from the dev team can share some lights here ?

twisted1919

Mike’s link is pretty descriptive about different types of storing persistent data.

As for shopping cart, you can store its contents in database while having login info only in the cookie.

About cookies and security. The idea is to regenerate cookie after logging in and don’t accept old one.

persistent storage = database

cookie state = cookie set via setState

from cookie.

from DB.

How?

Yes, i’ve read that topic several times, went through documentation and through google code a few more times and i think i got it.

This would require another database table, in where i store the user_id and the cart contents, so i can’t keep the cart contents using the Yii::app()->session, correct ?

Regarding the authentication and auto login thing, is there a snippet of code where we can look at, or better, can you give a short example ?

So i think i have the solution for cookie autologin thing:

First of all, i created a database table as follows:




CREATE TABLE IF NOT EXISTS `cms_autologin_tokens` (

  `token_id` int(11) NOT NULL AUTO_INCREMENT,

  `user_id` int(11) NOT NULL,

  `token` char(40) NOT NULL,

  PRIMARY KEY (`token_id`),

  KEY `user_id` (`user_id`)

) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

//this references the users table, no need for it, i add it for my case

ALTER TABLE `cms_autologin_tokens`

  ADD CONSTRAINT `cms_autologin_tokens_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `cms_user` (`user_id`) ON DELETE CASCADE ON UPDATE NO ACTION;



Next, the authentication method looks like:




    public function authenticate()

	{

		$this->password = $this->encrypt_password($this->password);


		$connection=Yii::app()->db;

        $command=$connection->createCommand('SELECT * FROM {{user}} WHERE email=:email AND password=:password AND is_active=1 LIMIT 1');

		$command->bindParam(':email',$this->email,PDO::PARAM_STR);

		$command->bindParam(':password',$this->password,PDO::PARAM_STR);

		$row=$command->queryRow(); 

		

		if(empty($row))

		{

            $this->errorCode = Yii::t('general','Invalid login credentials');

		}

		else

		{    

		    $row=(object)$row;  


			$this->_id=$row->user_id;

            $this->_username=$row->username;

            

            Yii::app()->user->setState('email', $row->email);

            Yii::app()->user->setState('last_ip', $row->last_ip);

            Yii::app()->user->setState('last_login', $row->last_login);

            

            if(Yii::app()->user->allowAutoLogin)

            {

                $autoLoginToken=sha1(uniqid(mt_rand(),true));

                $this->setState('autoLoginToken',$autoLoginToken);

                //delete old keys

                $command=$connection->createCommand('DELETE FROM {{autologin_tokens}} WHERE user_id=:user_id');

                $command->bindParam(':user_id',$row->user_id,PDO::PARAM_STR);

                $command->execute();

                //set new

                $command=$connection->createCommand('INSERT INTO {{autologin_tokens}}(token_id,user_id,token) VALUES(NULL,:user_id,:token)');

                $command->bindParam(':user_id',$row->user_id,PDO::PARAM_STR);

                $command->bindParam(':token',$autoLoginToken,PDO::PARAM_STR);

                $command->execute();

                    

            }

			$this->errorCode = self::ERROR_NONE ;

		}

		

		return ! $this->errorCode;

	}



And, the overwritten beforeLogin method:




    public function beforeLogin($id, $states, $fromCookie)

    {

        if($fromCookie)

        {

            if(empty($states['autoLoginToken']))

            {

                return false;

            }

            $autoLoginKey=$states['autoLoginToken'];

            $connection=Yii::app()->db;

            $command=$connection->createCommand('SELECT * FROM {{autologin_tokens}} WHERE user_id=:user_id ORDER BY token_id DESC LIMIT 1');

    		$command->bindParam(':user_id',$id,PDO::PARAM_STR);

    		$row=$command->queryRow(); 

            return !empty($row) && $row['token']===$autoLoginKey;

        }

        return true;

    } 



And it works excellent.

Any thoughts ?

You can turn on cookie validation:




'components'=>array(

  'request'=>array(

    'enableCookieValidation'=>true,

  ),

),



This will turn on HMAC validation for cookies and it will be enough to prevent cookie modification but alone will not save application from stealing cookies.

Yii::app()->session is a wrapper for session so data will be lost the same time user session will be closed.

Yes i know this, and i already have the cookie validation enabled.

Fair enough, it makes sense this way, which, implies that the decision of creating a special database table to store tokens for auto login from my previous post was correct .

So i guess this post really was helpful for me and for every user that will have same questions like i did .

You don’t require a DB table for a shopping cart. You still can use session and even access it with Yii::app()->user->setState() / getState().

Like i mentioned in the other post: I’m also unsure about what the concept of a “user state” (!== “user identity state”, which might be stored to cookie!) is good for. To me it’s just another wraper for the session. I tend to use user states often, because they seem to save me a call to open the session first.

EDIT: Correction … you don’t require a DB table, if the drawbacks of a session mentioned by samdark are acceptable to you.

Actually it will need a DB table in my case, because i want to keep the cart contents until the user removes them .

Otherwise, as you say, if the drawbacks mentioned by samdark are accepted, i think my cart example is pretty clear example of storing data using Yii::app()->session and NOT Yii::app()->user->setState().

I think this was the idea from the start, use CWebUser for user data ONLY and use the session class for everything else, like a shopping cart .

Hi,

I am new to Yii. Sorry for bringing up an old thread, but how can one test if the beforeLogin function is actually working and is doing what it suppose to do?


<?php


class MyCWebUser extends CWebUser{


    public function beforeLogin()

    {

         die("CWebUser::beforeLogin");

    }  


}

Hi jacmoe,

thanks for the prompt response. I tried your code block and it does nothing. What I am trying to do is implement the recommended approach for cookie-based login from http://www.yiiframework.com/doc/guide/1.1/en/topics.auth.

I override the two function authenticate() and beforeLogin(), but not exactly sure how to go about implementing the second recommendation which is the beforeLogin() which check that the cookie hash value is the same as that stored in the DB.

This is what I have so far:




public function beforeLogin()

{

   $validAutoLogin = false;

   $connection = Yii::app()->mipUsers;

		

   $sql = "select cookie_token from users where userid = :userid and username = :username";

		

   $command = $connection->createCommand($sql);

   $command->bindParam(":userid",    $this->_userid);

   $command->bindParam(":username",  $this->_username);


   $row = $command->queryRow();


   if($row['ac_session'] != Yii::app()->user->cookie_token)

   {

      $validAutoLogin = true;

   }

		

   return $validAutoLogin;

}


public function authenticate()

{

   $isAuthen = false;

		

   $connection = Yii::app()->siteUsers;

		

   $sql = "select hash, salt, cookie_token from users where userid = :userid and username = :username";

		

		$command = $connection->createCommand($sql);

		$command->bindParam(":userid",    $this->_userid);

		$command->bindParam(":username",  $this->_username);


		$row = $command->queryRow();

		

		if(gettype($row) === 'array' && !empty($row))

		{

			if(sha1($this->password . $row['salt']) == $row['hash'])

			{

				$this->_current_session = sha1($this->_username . '_' . date('F d, Y H:i:s'));

				

				$this->setState('cookie_token', $this->_current_session);


				$this->isAuthen = true;

				

				$sql = "update users set cookie_token = :cookie_token where userid = :userid and username = :username";

		

				$command = $connection->createCommand($sql);

				$command->bindParam(":cookie_token", $this->_current_session);

				$command->bindParam(":userid",       $this->_userid);

				$command->bindParam(":username",     $this->_username);

				$command->execute();

			}	

		}


		return $isAuthen;

}