Simple Authentication with the Yii Framework

January 4, 2010
The Yii Book If you like my writing on the Yii framework, you'll love "The Yii Book"!

I wanted to write up a strong post on Access Control Lists in Yii, for controlling who can do what in an application. I still will, of course, but since authorization depends upon authentication, I thought it necessary to discuss Yii’s authentication system first. And, as happens with me, in writing about that, and how you would [intlink id=”849″ type=”post”]customize the authentication process[/intlink], I had to break the subject down into two posts. So here’s the first of an informal three-part series on authentication and authorization in Yii. In this post, I discuss how the parts of Yii’s authentication system work together; much of what I say in this first post is also available in the Yii documentation, just not presented in this way. I also show a couple of quick ways to modify its behavior to suit your situation.

Most sites use some form of authentication. Maybe users have to login to post questions, to read content, or to administer the site. Whatever the case, a verification of the person is required before they can do whatever. As this is standard behavior, the default application created by the Yii framework has built-in authentication. When you generate a new site using Yii’s command-line tools, three files for managing authentication are created:

  • protected/components/UserIdentity.php
  • protected/models/LoginForm.php
  • protected/views/site/login.php

And there’s also some code added to protected/controllers/SiteController.php that comes into play. The Controller file, gets the action going, of course. The View file is the login form itself. The LoginForm Model file defines the rules and behavior and the UserIdentity file defines a Model that performs the actual authentication.

By default, if you do nothing at all, users will be allowed to log into the site using the username/password combinations of demo/demo or admin/admin. These values are set in the UserIdentity.php script. If all you need to do is allow only a single authenticated user, then you can open UserIdentity.php and change this code:

$users=array(
    // username => password
    'demo'=>'demo',
    'admin'=>'admin',
);

to

$users=array(
    'whateverName'=>'whateverPassword'
);

At that point, you’re done. However, you most likely will perform authentication against a database table, so let’s see how you’d edit these files to make that happen. To start, let’s assume that the table name is Users, that you’ve already generated a Model for it, and that authentication requires an username and a password. As you’ll see, you’ll still only need to edit the UserIdentity.php file to make this possible, but in my next post I’ll walk through more elaborate customizations. Okay, with that in mind, let’s examine the authentication process…

The URL to login will likely be www.example.com/index.php/site/login, as Yii puts login/logout functionality in the Site Controller by default. So when the user clicks on a link to go to the login page, they’ll go through the Site Controller and call the actionLogin() method. That method is defined as:

public function actionLogin()
{
    $form=new LoginForm;
    // collect user input data
    if(isset($_POST['LoginForm']))
    {
        $form->attributes=$_POST['LoginForm'];
        // validate user input and redirect to previous page if valid
        if($form->validate()  && $form->login()) $this->redirect(Yii::app()->user->returnUrl);
    }
    // display the login form
    $this->render('login',array('form'=>$form));
}

First, a new object is created of type LoginForm. That class is defined in the LoginForm.php Model file. If the form has been submitted, the form data is collected and the data is validated. If the data passes the validation, the user will be redirected to whatever URL got them here in the first place. If the form has not been submitted, or if the form data does not pass the validation routine, then the login form is displayed, and it is passed the LoginForm object. The default login form looks like this (note the reference to demo/demo and admin/admin, already discussed):

Yii Login Form

Now, the call to the validate() method in the above code means that the form data has to pass the validation rules established in the LoginForm class. This is basic Model validation as defined by the rules() method of the Model (also see [intlink id=”657″ type=”post”]my post on basic Model edits[/intlink]):

public function rules()
{
    return array(
        // username and password are required
        array('username, password', 'required'),
        // password needs to be authenticated
        array('password', 'authenticate'),
    );
}

As you can see, those rules say that both the username and password are required and that the password has to pass the authenticate() method. This is not a built-in validation routine, but is defined in LoginForm. The LoginForm::authenticate() method starts off like so:

public function authenticate($attribute,$params)
{
    if(!$this->hasErrors())  // we only want to authenticate when no input errors
    {
        $identity=new UserIdentity($this->username,$this->password);
        $identity->authenticate();
        switch($identity->errorCode)

If there are no errors when this method is called (an error would be a lack of a username or password), a UserIdentity object is created, passing that object the submitted username and password. Then the UserIdentity object’s authenticate() method is called. This gets us to protected/components/UserIdentity.php, which defines the class and the method. If you’re having trouble following the process thus far, here it is pictorially:

Yii Auth Process

Note that any errors stops the process from continuing and just re-displays the login form, with the errors reported.

As I already wrote above, the UserIdentity authenticate() method as written compares the submitted values against predefined combinations and returns a message accordingly. If you want to test the submitted values against the database, you’ll need to add some code. Start by fetching the record for the given username:

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

Now the $user object represents the User record with an username field equal to the submitted username. (The CUserIdentity class’s constructor takes the provided username and password and stores them in $this->username and $this->password, just to be clear.) You could, and might be inclined, to attempt to retrieve the record that matches both the username AND the password, but if you were to do that, you wouldn’t be able to provide more meaningful error messages: username doesn’t exist, username does exist but the password doesn’t match, and so forth.

Next the authenticate() method should check 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->pass !== 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 username 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;
}

Finally, the method returns a boolean value, indicating an error or not:

return !$this->errorCode;

The presence of an error code gets used in the authenticate() method of the LoginForm Model, in response to the authentication attempt (this is a continuation of the LoginForm::authenticate() code shown above):

$identity->authenticate();
switch($identity->errorCode)
{
    case UserIdentity::ERROR_NONE:
        $duration=$this->rememberMe ? 3600*24*30 : 0; // 30 days
        Yii::app()->user->login($identity,$duration);
        break;
    case UserIdentity::ERROR_USERNAME_INVALID:
        $this->addError('username','Username is incorrect.');
        break;
    default: // UserIdentity::ERROR_PASSWORD_INVALID
        $this->addError('password','Password is incorrect.');
        break;
}

So if there was no error, the user is logged in. The duration—the period for which they’ll be logged in—is either 30 days or for just the session, depending upon whether they checked the rememberMe box or not (see the login form image). If either error is present, it’ll be added to the current object, the validation will fail, meaning that the conditional in the actionLogin() method of SiteController.php will be false, and the login form will be rendered again, this time with the error message. The whole process therefore becomes:

Yii Auth Process, Complete

That’s an outline of the basic process, using a User Model (based upon a database table), instead of hard-coded values. All of this information can also be found in the Yii documentation, just not written quite like this. To take this information further, and to make it more practical, my next post will show you how to modify all of the Yii-generated code in order to:

  • Store the user’s ID so it can be referenced as they peruse the site
  • Store the user’s “role”, like reader, editor, and writer, as well.
  • Authenticate the user with their email address and password, not their username.

That post will follow in just a couple of days. EDIT: [intlink id=”849″ type=”post”]Here it is[/intlink]! As always, thanks for your interest in what I have to say and let me know if you have any questions or comments. Thanks, Larry