Cook your own User Authentication in Yii - Part 1

This is the first of  four articles demonstrating how to build your own simple User Authentication system by extending core Yii classes and just a few views. There are a number of Extensions, Modules and RBAC systems available in the Yii extensions directory and, of course, the built-in Yii RBAC scheme. But I've found that for the medium sized system, all of these are often too complicated and difficult to integrate.

What we are going to build

Autogenerated Yii applications have a very basic User Authentication system that is easily extended to provide a database enabled user authentication and in this series of articles we will be starting from this base. We will build a simple level based User security system where a user role might be along the lines of subscriber, author, moderator or administrator and we will be able to use these levels to limit access to functions within our applications. User Passwords will be checked against a strength algorithm. Users will be forced to change their passwords after a set time interval.

Understanding the difference between WebUser and Authentication

webuser-authentication 

This first part will be to understand the difference between User Authentication and the users Web Identity. User information is stored in an instance of the CWebUser class and this is created on application initialisation (ie: when the User first connects with the website), irrespective of whether the user is logged in or not. By default, the user is set to “ Guest”. 

Authentication is managed by a class called CUserIdentity and this class checks that the user is known and a valid user. How this validation occurs will depend on your application, perhaps against a database, or login with facebook, or against an ldap server etc... 

The code generated by Gii defines the login action and loginForm model which manages this process for us and ties these two classes together. On login, the System creates a UserIdentity class, passing the login details. These are validated against, in this example, a database. 

The login model then passes the UserIdentity object to the CWebUser object, which then stores this information. By default the CWebUser class stores its information in session data and therefore, should NOT contain any sensitive information, like passwords.

Our User Model

So our first step is to define our User model and then generate a model and CRUD actions using Gii. Here is my User table:
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(128) NOT NULL,
  `password` varchar(128) NOT NULL,
  `email` varchar(128) NOT NULL,
  `firstname` varchar(128) DEFAULT NULL,
  `lastname` varchar(128) DEFAULT NULL,
  `pagination` tinyint(3) NOT NULL DEFAULT '25',
  `role` int(1) NOT NULL DEFAULT '0',
  `create_date` datetime NOT NULL,
  `last_login_time` datetime NOT NULL,
  `status` int(1) NOT NULL DEFAULT '0',
  `password_expiry_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`),
  UNIQUE KEY `email` (`email`),
  KEY `status` (`status`),
  KEY `superuser` (`role`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
Now use Gii to generate model and CRUD forms...

Customising the User Model

now we need to update the User model as follows; 1.  Include constants to represent our ROLES 2.  Add fields and validation for repeat passwords, so that on our forms we can ask the user to enter their passwords twice 3.  Password strength algorithm 4.  Combine Firstname and Lastname into FullName 5.  Use beforeSave to check passwords and update password expiry date 6.  Add any relevant relations for your application
<!--?php
/**
 * This is the model class for table "users".
 *
 * The followings are the available columns in table 'users':
 * @property integer $id
 * @property string $username
 * @property string $password
 * @property string $email
 * @property string $pagination
 * @property integer $createtime
 * @property integer $last_login_time
 * @property integer $role
 * @property integer $status
 *
 * The followings are the available model relations:
 * @property Customer[] $customers
 * @property Issues[] $issues
 * @property Issues[] $issues1
 */
class Users extends CActiveRecord
{
 const ROLE_AUTHOR=1;
 const ROLE_MODERATOR=3;
 const ROLE_ADMIN=5;
 const PASSWORD_EXPIRY=90;
 public $passwordSave;
 public $repeatPassword;
 /**
  * Returns the static model of the specified AR class.
  * @param string $className active record class name.
  * @return Users the static model class
  */
 public static function model($className=__CLASS__)
 {
 return parent::model($className);
 }
 /**
  * @return string the associated database table name
  */
 public function tableName()
 {
 return 'users';
 }
 public function fullName() {
            $fullName=(!empty($this->firstname))? $this->firstname : '';
            $fullName.=(!empty($this->lastname))?( (!empty($fullName))? " ".$this->lastname : $this->lastname ) : '';
            return $fullName;
        }
 /**
  * @return array validation rules for model attributes.
  */
 public function rules()
 {
 // NOTE: you should only define rules for those attributes that
 // will receive user inputs.
 return array(
  array('passwordSave, repeatPassword', 'required', 'on'=>'insert'),
  array('passwordSave, repeatPassword', 'length', 'min'=>6, 'max'=>40),
  array('passwordSave','checkStrength','score'=>20),
  array('passwordSave', 'compare', 'compareAttribute'=>'repeatPassword'),
  array('email','email'),
  array('username, password, email', 'required'),
  array('role, status, pagination', 'numerical', 'integerOnly'=>true),
  array('username, firstname, lastname', 'length', 'max'=>128),
  array('password, email', 'length', 'max'=>128),
  array('last_login_time, create_date', 'safe'),
  // The following rule is used by search().
  // Please remove those attributes that should not be searched.
  array('id, username, password, email, pagination, createtime, last_login_time, role, status', 'safe', 'on'=>'search'),
  );
 }
 /** score password strength
  * where score is increased based on
  * - password length
  * - number of unqiue chars
  * - number of special chars
  * - number of numbers
  * 
  * A medium score is around 20
  * 
  * @param type $attribute
  * @param type $params
  * @return boolean 
  */
 function CheckStrength($attribute,$params) 
 {
 if (!empty($this->$attribute)) {  // Edit 2013-06-01
  $password=$this->$attribute;
  if ( strlen( $password ) == 0 )
  $strength=-10;
  else
  $strength = 0;
  /*** get the length of the password ***/
  $length = strlen($password);
  /*** check if password is not all lower case ***/
  if(strtolower($password) != $password)
  {
  $strength += 1;
  }
  /*** check if password is not all upper case ***/
  if(strtoupper($password) == $password)
  {
  $strength += 1;
  }
  /*** check string length is 8 -15 chars ***/
  if($length >= 8 && $length <= 15)
  {
  $strength += 2;
  }
  /*** check if lenth is 16 - 35 chars ***/
  if($length >= 16 && $length <=35)
  {
  $strength += 2;
  }
  /*** check if length greater than 35 chars ***/
  if($length > 35)
  {
  $strength += 3;
  }
  /*** get the numbers in the password ***/
  preg_match_all('/[0-9]/', $password, $numbers);
  $strength += count($numbers[0]);
  /*** check for special chars ***/
  preg_match_all('/[|!@#$%&*\/=?,;.:\-_+~^\\\]/', $password, $specialchars);
  $strength += sizeof($specialchars[0]);
  /*** get the number of unique chars ***/
  $chars = str_split($password);
  $num_unique_chars = sizeof( array_unique($chars) );
  $strength += $num_unique_chars * 2;
  /*** strength is a number 1-100; ***/
  $strength = $strength > 99 ? 99 : $strength;
  //$strength = floor($strength / 10 + 1);
  if ($strength<$params['score']) 
  $this->addError($attribute,"Password is too weak - try using CAPITALS, Num8er5, AND sp€c!al characters. Your score was ".$strength."/".$params['score']); 
  else
  return true;
  }
}
 public function beforeSave() {
 parent::beforeSave();
 //add the password hash if it's a new record
 if ($this->isNewRecord) {
     $this->password = md5($this->passwordSave); 
     $this->create_date=new CDbExpression("NOW()");
     $this->password_expiry_date=new CDbExpression("DATE_ADD(NOW(), INTERVAL ".self::PASSWORD_EXPIRY." DAY) ");
 }       
 else if (!empty($this->passwordSave)&&!empty($this->repeatPassword)&&($this->passwordSave===$this->repeatPassword)) 
 //if it's not a new password, save the password only if it not empty and the two passwords match
 {
     $this->password = md5($this->passwordSave);
     $this->password_expiry_date=new CDbExpression("DATE_ADD(NOW(), INTERVAL ".self::PASSWORD_EXPIRY." DAY) ");
 }
 return true;
 }
 /**
  * Compare Expiry date and today's date
  * @return type - positive number equals valid user
  */
 public function checkExpiryDate() {
 $expDate=DateTime::createFromFormat('Y-m-d H:i:s',$this->password_expiry_date);
 $today=new DateTime("now");
 fb($today->diff($expDate)->format('%a'),"PASSWORD EXPIRY");
 return ($today->diff($expDate)->format('%a'));
 }
 /**
  * @return array relational rules.
  */
 public function relations()
 {
 // NOTE: you may need to adjust the relation name and the related
 // class name for the relations automatically generated below.
 return array(
 );
 }
 /**
  * @return array customized attribute labels (name=>label)
  */
 public function attributeLabels()
 {
 return array(
 'id' => 'ID',
 'username' => 'Username',
 'password' => 'Password',
 'email' => 'Email',
 'pagination' => 'pagination',
 'role' => 'role',
 'status' => 'Status',
 'create_date' => 'Createtime',
 'last_login_time' => 'last_login_time',
 'passwordSave' => 'Password', 
 'passwordRepeat' => 'Repeat Password', 
 );
 }
 /**
  * Retrieves a list of models based on the current search/filter conditions.
  * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
  */
 public function search()
 {
 // Warning: Please modify the following code to remove attributes that
 // should not be searched.
 $criteria=new CDbCriteria;
 $criteria->compare('id',$this->id);
 $criteria->compare('username',$this->username,true);
 $criteria->compare('password',$this->password,true);
 $criteria->compare('email',$this->email,true);
 $criteria->compare('pagination',$this->pagination,true);
 $criteria->compare('create_date',$this->create_date);     // Edit 2013-06-01
 $criteria->compare('last_login_time',$this->last_login_time);
 $criteria->compare('role',$this->role);
 $criteria->compare('status',$this->status);
 return new CActiveDataProvider($this, array(
 'criteria'=>$criteria,
 ));
 }
}

The userIdentity class

Next we will change the original Gii generated userIdentity class to authenticate the user against our new User table. This is found in Protected/Compontents/UserIdentity.php and will look like this:-
/**
 * UserIdentity represents the data needed to identity a user.
 * It contains the authentication method that checks if the provided
 * data can identity the user.
 */
class UserIdentity extends CUserIdentity
{
 /**
  * Authenticates a user.
  * Authenticate against our Database
  * @return boolean whether authentication succeeds.
  */
        private $_id;
        private $_username;
        public function getName()
        {
           return $this->_username;
        }
        public function getId()
        {
           return $this->_id;
        }
        public function authenticate()
        {
            $user= Users::model()->find('LOWER(username)=?', array(strtolower($this->username)));
            if($user === null)
             {
                 $this->errorCode= self::ERROR_UNKNOWN_IDENTITY;
             }
             elseif($user->password !== md5($this->password))
             {
                 $this->errorCode= self::ERROR_PASSWORD_INVALID;
             }
             else
             {
                 $this->_id = $user->id;
                 $this->_username = $user->email;
                 $user->last_login_time=new CDbExpression("NOW()");
                 $user->save();
                 $this->errorCode= self::ERROR_NONE;
             }
                 return !$this->errorCode;
         }
    } 

We now have a basic but working database Authentication system and in the next Tutorial I'll be extending the CWebUer class  


sign-up to my newsletter for more interesting tutorials 


credits: I found the CheckStrength algorithm on Snipplr.com with a deadlink, so I cannot properly credit the original author

-->

Did you know you can hire me?

I take on projects of all sizes. From Consulting to large Development Projects.

If you're starting a new Yii project and would like some help to get setup and running or you need some help with a particular module or you just need someone to develop the whole dang thing, then just ask ...


One comment

  • chris
    30/11/00-1

    Antonio wrote to me with an error 403 when trying to access a controller. He found the solution as follows:-

    Actually, on your tutorial - part 1, under userIdentity interface you have:
    $this->_username = $user->email;
    And on my accessRules() I had:

    <pre>
    array('allow', // allow admin user to perform 'admin' and 'delete' actions
    'actions'=>array('admin','delete'),
    'users'=>array('admin'),
    ),
    </pre>

    So, I had: 'users'=>array('admin'),
    Since ‘admin’ is NOT an email, it doesn’t work.

    So, under userIdentity, I change to:
    $this->_username = $user->username;
    And since my username name is: ‘admin’, I don’t get a 403 error.
    So, issue solved. :)

Leave a Comment

twitterfacebookgooglelinkedin https://me.yahoo.com