Wednesday, March 17, 2010

Yii Authentication via database

Yii has a wonderful AuthManager built in, but it's a little confusing to use so many people end up creating their own. This one drove me a little nuts for a bit, so I thought it would be a good place to start.   For the purposes here, I'm going to assume you've already added a db setting to your main Controller. For more information on how to do that, read here.

This is the starting point for today's adventure: http://www.yiiframework.com/doc/guide/topics.auth

The article above walks you through adjusting the UserIdentity (found in /protected/components/UserIdentity.php ) to pull information from a database table for users, override the ID of the UserIdentity class and set up some basic authentication rules, but you have to read between the lines to get it all complete.

Prepwork!

If you've already got a User model set up, you can (Skip to the auth details)
Before modifying your UserIdentity, you need to first create a users table with a keyed ID field.  I use MySQL, you can use whatever you like for a backend. The table simply needs to have (at a minimum) an id, username and password column, such as:
CREATE TABLE User( 
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(45),
`password` VARCHAR(254),
`title` VARCHAR(45),
PRIMARY KEY(`id`))
ENGINE = MyISAM;
I've included a title field simply because the yii guide example includes it and it's a good example of how to add additional data to your authenticated user. Obviously, in a real life situation you're going to want more information stored for your User, but this is the bare minimum to get the ball rolling.

To be easily able to manage and update your users, you should now open up the yiic shell and model Users. So, at the command prompt from your /protected directory:
$  ./yiic shell config/main.php
$  model User
$  crud User

You now have a fully manageable set of user identities which you can edit and add to via web form. (The model command creates the data model for the table User and calls the new object class User, if you need to change the table name, simply issue the command as: model User MyDB.MyTableName )

NOTE: (futher explaination of the above code)
The model command will generate the User model based on your database table that you created. It will also create for you the Unit Test class and fixture data for the User model.

The crud command will generate a UserController along with the preset CRUD actions for Create, Read, Update and Delete (as well as your standard search/admin).

These commands should be typed from within your web application's /protected/ directory, or if using the main webapp directory, then type: protected/yiic shell

To view your users list, you should be able to pull up: /index.php/User/ or index.php?r=User (depending on whether you've configured Yii to alter the path names)

You can now go back to the authentication guide linked above and alter your UserIdentity to access the User table and check access like so:
class UserIdentity extends CUserIdentity
{
    private $_id;
    public function authenticate()
    {
        $record=User::model()->findByAttributes(array('username'=>$this->username));
        if($record===null)
            $this->errorCode=self::ERROR_USERNAME_INVALID;
        else if($record->password!==md5($this->password))
            $this->errorCode=self::ERROR_PASSWORD_INVALID;
        else
        {
            $this->_id=$record->id;
            $this->setState('title', $record->title);
            $this->errorCode=self::ERROR_NONE;
        }
        return !$this->errorCode;
    }
 
    public function getId()
    {
        return $this->_id;
    }
}
NOTE the override for getId -- this is VERY important for the authentication systems later.

Now that that's out of the way, we can actually start telling the system *which* users have set access.

Now it's time to add more tables to the database for Authentication rule storage. In your main yii directory is a file called framework/web/auth/schema.sql . Run this file through your mysql command prompt (or editor of choice) and you will have three new tables set up for Authentication Management. They are: AuthAssignment, AuthItem, AuthItemChild

Finally, we're ready to go back to the main authentication guide and continue!!

Authentication

Move down to this section of the AuthManager guide, and follow the instructions for configuring your AuthManager as shown.

Authentication in Yii has three "levels"- operations, tasks and roles.

We're going to step through setting up 3 basic roles. An admin role, an authenticated role, and a guest role. With this, we can add in infinite customized authentication tasks/operations for all our needs. The admin role will be specifically assigned to user IDs from our Users table.

It's time to head back into the yiic shell and set up our basic roles. If you prefer, you can enter these directly into the database tables you created, or copy the code into a php page which you run ONE TIME ONLY.
(For the purposes of this example, we will assume that you, the primary admin, are user id 1)
The shell version(running this from the shell automatically saves it, if you run it from a php page, I believe you need to call $auth->save() at the end -- I always use the shell (or hit the database directly which is a bit hack)):
$auth=Yii::app()->authManager;

$bizRule='return !Yii::app()->user->isGuest;';
$auth->createRole('authenticated', 'authenticated user', $bizRule);
 
$bizRule='return Yii::app()->user->isGuest;';
$auth->createRole('guest', 'guest user', $bizRule);

$role = $auth->createRole('admin', 'administrator');
$auth->assign('admin',1); // adding admin to first user created

The admin role is, as we said, assigned directly. The other two are polar opposites which will be checked and applied to all users as they load the site to determine whether they are authenticated or not. EVERY user therefor will fall into one of those two categories and can access tasks/operations assigned to authenticated or guest users.
To add in the default roles, open up the config/main.php file and modify the array where you added the AuthManager as follows:
'authManager'=>array(
            'class'=>'CDbAuthManager',
            'defaultRoles'=>array('authenticated', 'guest'),
        ),
Now whenever someone comes to the site, they'll automatically be put into one of those two categories since they use the same business rule but one is for true and one for false, and your user id 1 will have all the permissions that an admin has.

Great ... but it doesn't actually DO anything yet.

In your Controllers (we'll use the User as an example since we created that one above) you can now change the /protected/controllers/UserController.php accessRules function to allow only your admin to delete users as such:
public function accessRules(){
    return array(
        array('allow', // allow anyone to register
              'actions'=>array('create'), 
              'users'=>array('*'), // all users
        ),
        array('allow', // allow authenticated users to update/view
              'actions'=>array('update','view'), 
              'roles'=>array('authenticated')
        ),
        array('allow', // allow admins only to delete
              'actions'=>array('delete'), 
              'roles'=>array('admin'),
        ),
        array('deny', // deny anything else
              'users'=>array('*'),
        ),
    );
}
But, we don't want the users to edit each other! This is where tasks/operations come into play.

We need a task which allows users to update their own information. Back to the shell:
$auth=Yii::app()->authManager;
$bizRule = 'return Yii::app()->user->id==$params["User"]->id;';
$auth->createTask('updateSelf', 'update own information', $bizRule);

$role = $auth->getAuthItem('authenticated'); // pull up the authenticated role
$role->addChild('updateSelf'); // assign updateSelf tasks to authenticated users

Finally...

Open the UserController.php file again and move to the actionUpdate() function. We'll need to modify it as such:
public function actionUpdate()
{
    $model = $this->loadModel();
    
    // set the parameters for the bizRule
    $params = array('User'=>$model);
    // now check the bizrule for this user
    if (!Yii::app()->user->checkAccess('updateSelf', $params) &&
        !Yii::app()->user->checkAccess('admin'))
    {
        throw new CHttpException(403, 'You are not authorized to perform this action');
    }
    ...  

That's pretty much it! So for simple role or operation based access, changing the accessRules will do it, for more complex logic with a business rule, assign the task to the default role, then pull it up in the specific action. I'm sure there's a way to get the accessRules to be smarter, but I haven't figured it out yet! When I do, I'll modify this.