Custom Authentication using the Yii Framework

January 7, 2010 — 121 Comments
The Yii Book If you like my writing on the Yii framework, you'll love "The Yii Book"!

In a previous post, I walk through the Yii framework’s built-in authentication system for adding login functionality to a Web site. There are a number of files and components involved, but simple authentication works fine out of the box, or with just a little tweaking. That’s the focus of that previous post. In this post, I explain how you can customize the authentication process further.The default authentication behavior allows users to login with hardcoded username/password combinations. In this post, I want to change that behavior so that:

  • Authentication is performed against a database table
  • The user’s email address is used instead of their username
  • The user’s ID is stored for later reference
  • The user’s “role” is stored for later reference

To start, let’s assume there is a database table called User that’s already been modeled and populated. The table has several columns, including at least: id, email, password, and role. The password column stores a SHA1()-encrypted version of the user’s password. The role dictates what the user can do in the site. Possible values are reader, editor, and writer (these are hypothetical values; they could be anything).

Now, by default, Yii will use cookies for authentication. In most situations that’s fine, but if anything of a sensitive nature is being stored, you should use sessions instead. This would apply to both the user’s ID value and their role. If either is available through a cookie, it wouldn’t be hard for the user to edit that cookie’s value in order to become someone else. So, to start, let’s disable the potential for using cookies. To do that, open up the protected/config/main.php configuration file and find this section from under components:

'user'=>array(
    // enable cookie-based authentication
    'allowAutoLogin'=>true,
),

To disable cookie-based authentication, either remove that code entirely, or change allowAutoLogin to false.

Next, let’s turn to the login form, which will require a couple of alterations. Open up protected/views/site/login.php, which is the form. The default form looks like this:

Yii Login Form

First, we need to remove the hint paragraph, as demo/demo and admin/admin will no longer work. Then we remove the code that displays the “remember me” checkbox. Remember me functionality is only good for cookies, so it’s useless here. Finally, we want to take an email address, not a username, so those two lines must be changed. The complete form file is now:

<?php $this->pageTitle=Yii::app()->name . ' - Login'; ?>

<h1>Login</h1>

<div>
<?php echo CHtml::beginForm(); ?>

<?php echo CHtml::errorSummary($form); ?>

<div>
<?php echo CHtml::activeLabel($form,'email'); ?>
<?php echo CHtml::activeTextField($form,'email') ?>
</div>

<div>
<?php echo CHtml::activeLabel($form,'password'); ?>
<?php echo CHtml::activePasswordField($form,'password') ?>
</div>

<div>
<?php echo CHtml::submitButton('Login'); ?>
</div>

<?php echo CHtml::endForm(); ?>

</div><!-- yiiForm -->

Next, we turn to the LoginForm Model, defined in protected/models/LoginForm.php. This Model is associated with the login form, handling and validating that submitted data. At the top of the Model, the class variables are defined. We need to change $username to $email and remove $rememberMe:

class LoginForm extends CFormModel
{
    public $email;
    public $password;

Next, alter the rules accordingly. Instead of username and password being required, email and password are required. Also, the email value should be in a valid email address format. The application of the authenticate() method to validate the password remains. Here are the updated rules:

public function rules()
{
    return array(
        array('email, password', 'required'),
        array('email', 'email'),
        array('password', 'authenticate'),
    );
}

Next, if you want, change the attributeLabels() method for the email address and remove the label for rememberMe:

public function attributeLabels()
{
    return array('email'=>'Email Address');
}

The final changes to the LoginForm Model are in the authenticate() method. Several references to username must be changed to email. For example, this:

$identity=new UserIdentity($this->username,$this->password);

becomes:

$identity=new UserIdentity($this->email,$this->password);

Then there’s a call to UserIdentity::authenticate(), which is where the actual authentication against the database takes place (see my previous post and I’ll also return to this shortly). After that, there’s an important switch conditional that responds to three possibilities: authenticated, invalid username, and invalid password. The applicable case is signaled by a UserIdentity constant. Do note that the UserIdentity class is an extension of CUserIdentity, so it uses ERROR_USERNAME_INVALID instead of ERROR_EMAIL_INVALID. In the following code I treat username and email address as synonymous, because it’s the easiest solution. A full alteration would require changing the Yii framework’s definition of CBaseUserIdentity (which CUserIdentity extends), which is not a good idea. So here’s the modified switch:

switch($identity->errorCode)
{
    case UserIdentity::ERROR_NONE:
        Yii::app()->user->login($identity);
        break;
    case UserIdentity::ERROR_USERNAME_INVALID:
        $this->addError('email','Email address is incorrect.');
        break;
    default: // UserIdentity::ERROR_PASSWORD_INVALID
        $this->addError('password','Password is incorrect.');
        break;
}

In the first case, with no error present, the user is logged in. I’ve removed references to rememberMe and duration, both of which are present in the Yii-generated code. The second case applies if the email address was not found in the database. Again, the error code is ERROR_USERNAME_INVALID, but it applies just the same. We do want to change the error so that it applies to the email element and has the proper error message. The final case applies if the email address was found but the supplied password was incorrect. Here’s an image for how the login form looks and behaves after these modifications:

Modified Login Form with Errors

Finally, we turn to protected/components/UserIdentity.php, for the final changes. There are a couple of things we need to do in this file. First, we need to perform the authentication against the User Model (and therefore, the database). Second, we need to store the user’s ID and role in the session for later use in the site. For the authentication, we’ll modify the authenticate() method:

public function authenticate()
{
    $user = User::model()->findByAttributes(array('email'=>$this->username));

Now the $user object represents the User record with an email field equal to the submitted email address. You may be wondering why I refer to $this->username here. That’s because the CUserIdentity class’s constructor takes the provided email address and password (from LoginForm) and stores them in $this->username and $this->password. So I need to equate username with email here, which is better than editing the framework itself. You ought to leave a comment about this so that you won’t be confused later when looking at the code.

Next the authenticate() method checks a series of possiblities and assigns constant values to the errorCode variable:

if ($user===null) { // No user found!
    $this->errorCode=self::ERROR_USERNAME_INVALID;
} else if ($user->password !== SHA1($this->password) ) { // Invalid password!
    $this->errorCode=self::ERROR_PASSWORD_INVALID;

In the first conditional, if $user has no value, then no records were found, so the email address was incorrect. In the second conditional, the stored password is compared against the SHA1() version of the submitted password. This assumes the record’s password was stored in a SHA1()-encrypted format. If neither of these conditionals are true, then everything is okay:

} else { // Okay!
    $this->errorCode=self::ERROR_NONE;
    // Store the role in a session:
    $this->setState('role', $user->role);
}

As you can see, a constant representing no error is assigned to the error code. After that, the user’s role value, from the database table, is stored in the session. This is accomplished by invoking the setState() method. Provide it with a name, role, and a value. After you’ve done this, the user’s role will be available through Yii::app()->user->role. You could do the same thing to store the user’s ID in the session, but the built-in authentication already has a getId() method that returns the user’s identifier. By default, the method returns the username value, so you’ll need to override the default behavior to return the ID instead. Start by creating a private variable in UserIdentity:

class UserIdentity extends CUserIdentity
{
    // Need to store the user's ID:
    private $_id;

Then, in the else clause (for successful authentication), assign the user’s ID to the class ID variable:

$this->_id = $user->id;

Finally, after the authenticate() method, override the getId() method by redefining it as:

public function getId()
{
    return $this->_id;
}

Now the user’s ID will be available through Yii::app()->user->id.

And that should be it. You now have an authentication process based upon an email address and password combination, using a database table, that also stores two pieces of information in the session. As always, thanks for reading and let me know if you have any questions or comments. Thanks, Larry!

Edit: Per a request, here’s the database schema, the LoginForm.php Model file, and the components/UserIdentity.php script that I *think* I used for this post:

CREATE TABLE `User` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `email` varchar(80) NOT NULL,
 `pass` char(40) NOT NULL,
 `role` enum('reader','editor','writer') NOT NULL,
 PRIMARY KEY (`id`)
);

LoginForm.php

UserIdentity.php

If you enjoyed this post, then please consider following me using your favorite social media, the RSS feed, and/or by subscribing to my newsletter. Or go crazy, and buy one or more of my books . Thanks!

121 responses to Custom Authentication using the Yii Framework

  1. Hi larry,
    Thank you very much for the wonder full tutorial.
    Is there any restriction that the table name must only be ‘user’, can i name is as ‘users’. Cause i am using the RIGHTS plugin, its showing some errors that it can’t find the user table.

  2. Hello Larry!
    I’ve downloaded your model LoginForm.php and tested. Unfortunately your decision have one problem. If function login deleted from model, we must delete a piece of code (&& $model->login) from line of controller:
    if($model->validate() && $model->login)
    But in this case user cannot see errors because he will redirected to returnUrl (or home page).
    So function login is important tool.

  3. sir i directly placed your files . but it gives an error while clicking on login

    Property “LoginForm.username” is not defined.

    /usr/share/nginx/www/yii1/framework/web/helpers/CHtml.php(2255)

    2243 $name=substr($attribute,0,$pos);
    2244 $value=$model->$name;
    2245 foreach(explode(‘][',rtrim(substr($attribute,$pos+1),']‘)) as $id)
    2246 {
    2247 if((is_array($value) || $value instanceof ArrayAccess) && isset($value[$id]))
    2248 $value=$value[$id];
    2249 else
    2250 return null;
    2251 }
    2252 return $value;
    2253 }
    2254 else
    2255 return $model->$attribute;
    2256 }
    2257

    i have configured database also.

    i am requesting you to please upload your yii login-authentication project folder . so i will try executing here. this is very important for me . kindly do favor

  4. Hey Larry. Great work with the full explanation! One question; can I still access the user’s name with Yii::app()->user->name anywhere in the site, and by $model->username within the user views? I just figured out how to be able to use both name and ID everywhere on the site, and before I go and mess things up (me, git and windows are not getting along), I would like to know if this will store the email in the username variable. Is my thinking right?

    (If the answer is no, you’ll see me in the forums!! :)

    • Thanks for the nice words. No, you cannot access the user’s name through Yii::app()->user->name if you change the login system to use their email address. It will return the email address instead. As for $model->username, assuming $model is of a user type with a username attribute, that would still work.

      • I was just about to try right now… glad I checked here first. Thanks for the quick reply.
        Ya, I was pretty sure we’re storing the email address in the same place that getName() method will look. After playing around with the user model and reading up on the official guide, though, Yii is starting to seem real smart in its design. I’m just figuring out how it accesses data from the database, so customizing shouldn’t be too bad.

        Thanks again for the tutorial and getting us all off on the right foot.

        • So I just used the same override technique that we used above to return the user’s id…. just used _name instead of _id and username for id. It’s all in this tutorial, just gotta look in the right places! Good stuff man, gonna have to invest in your book!

  5. HI Larry , Pls help . Tnx in advance .

    The login mechanism is not working for me , i get below error when replaced file protected/views/site/login.php with content mentioned above . Please help

    “Fatal error: Call to a member function getErrors() on a non-object in C:\xampp\htdocs\yii\framework\web\helpers\CHtml.php on line 1765″

  6. i user this code but my email field show me incorrect email

  7. hi ,

    i have following error

    Error 500

    include(users.php) [function.include]: failed to open stream: No such file or directory

    i have ct_registration table and users as a view for ct_registration.

    public function authenticate()
    {
    $user = users::model()->findByAttributes(array(‘username’=>$this->username));

    if(!isset($users[$this->username]))
    $this->errorCode=self::ERROR_USERNAME_INVALID;
    elseif($users[$this->username]!==$this->password)
    $this->errorCode=self::ERROR_PASSWORD_INVALID;
    else
    $this->errorCode=self::ERROR_NONE;
    $this->_id = $user->id;
    return !$this->errorCode;
    }

  8. Unless I am missing something, this does not work with role based access controle. Am I missing something, or is there a way to tie this method in to the access control?

    • I found a solution. In the accessRules() method of each controller, use this in place of ‘roles’:

      ‘expression’=>’isset(Yii::app()->user->role) && (Yii::app()->user->role == “ADMIN”)’,

Comments are great, but I'd strongly prefer any requests for assistance get made in the support forums. Thanks!