Cook your own User Authentication in Yii – Part 4

I know it’s been a long break since the previous articles,but a t last, here is the final article in the Series “Cook your own User Authentication in Yii”.

Previously

We already have a fully working User Authentication system.  A user can signup, login and change their profile and passwords.  We added a user menu in our main layout. In the user model we added functionality to check the password strength and on-save whether the user had changed their password or not.

In this tutorial

In this session, we’re going to add the functionality to confirm a user’s email address using an email with an activation link.  When they click on that link, we will add a function to activate their account and then log them in.

The Users table

We need to add this Activation code to the users table:-

ALTER TABLE `test`.`users`  
    ADD COLUMN `activate` VARCHAR(128) NULL AFTER `role`,

The Users data model

So this column needs to be added to the data model:-

  public function rules()
  {
    // NOTE: you should only define rules for those attributes that
    // will receive user inputs.
    return array(
    .....
    array('password, email, activate', 'length', 'max'=>128),
    .....
   }

User Status

we will need to set the status of the user to inactive at registration and then update it to active at activation

Add a constant at the top of the Users model:

   const STATUS_ACTIVE=1;<br />

note: Our original table definition was defined as setting status=0 by default therefore we do not need to add this to the before save event for a new record.

Activation email

I prefer to keep this kind of function in the data model as it is more Business function than Operational function.  You could place it in the controller, it’s up to you …

public function sendActivation() {
      $name='=?UTF-8?B?'.base64_encode($this->username).'?=';
      $subject='=?UTF-8?B?'.base64_encode('Signup Activation for example.com').'?=';
      $headers="From: Signups <[email protected]>rn".
        "Reply-To: {[email protected]}rn".
        "MIME-Version: 1.0rn".
        "Content-type: text/plain; charset=UTF-8";
      $body="Dear ".$this->username."rn";
      $body.="Thank you for signing up to example.com. rn rn";
      $body.="Please click this <a href="http://example.com/user/activate?a=$this->activate">link</a> to activate your account rn";
      $body.="or copy and paste the following link into your browser rn rn";
      $body.="http://example.com/user/activate?a=".$this->activate." rn rn";
      $body.="Many thanks";
      $body.="A Yii Developer";
      mail('[email protected]',$subject,$body,$headers);
  }
</[email protected]>

We will need to generate the Activation code in the before-save event for new records.  I’ve also added a default ROLE.

        
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) ");
        // default ROLE and Activate code
        $this->role=Users::ROLE_AUTHOR;
        $this->activate = md5( rand(9999,999999));
    }       
  ....
}

Lastly, while we are here, I just noticed a bug in the validation, where we have specified that the password is “required”, so we need to remove that

/**
   * @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'),
      // remove PASSWORD from required
      array('username, email', 'required'),
      array('role, status, pagination', 'numerical', 'integerOnly'=>true),
      array('username, firstname, lastname', 'length', 'max'=>128),
      array('password, email, activate', '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'),
    );
  }

The Users Controller

We need to update the Create action in the Users controller to send the activation email.

  /**
   * Creates a new model.
   * If creation is successful, the browser will be redirected to the 'view' page.
   */
  public function actionCreate()
  {
      //CB
    $model=new Users;
    // Uncomment the following line if AJAX validation is needed
    // $this->performAjaxValidation($model);
    if(isset($_POST['Users']))
    {
      $model->attributes=$_POST['Users'];
      if($model->save()) {
          Yii::app()->user->setFlash('saved', "Data saved!");
          // send activation email and then re-direct
          $model->sendActivation();
          $this->redirect(array('update','id'=>$model->id));} 
      else {
                            Yii::app()->user->setFlash('failure', "Data Not saved!");
                        }
    }
    $this->render('create',array(
      'model'=>$model,
    ));
  }

And then in the Update Action we need to check that a non-admin user can only access their own profile. By changing the ID of the URL, they could access any other User’s profile

  /**
   * Updates a particular model.
   * If update is successful, the browser will be redirected to the 'view' page.
   * @param integer $id the ID of the model to be updated
   */
  public function actionUpdate($id)
  {
    // Add check that this user is accessing their own profile only
    if (Yii::app()->user->id!=$id && !Yii::app()->user->isAdmin())
        throw new CHttpException(404, "Page not Found");
    $model=$this->loadModel($id);
    // Uncomment the following line if AJAX validation is needed
    $this->performAjaxValidation($model);
    if(isset($_POST['Users']))
    {
      $model->attributes=$_POST['Users'];
      if($model->save()) {
          Yii::app()->user->setFlash('saved', "Data saved!");
                        } else {
          Yii::app()->user->setFlash('failure', "Data Not saved!");
                        }
    }
    $this->render('update',array(
      'model'=>$model,
    ));
  }

And next, we can add the Activate action that is called when the user clicks on the link in the email

   public function actionActivate($a) {
      if ($a!='') {
    $model=Users::model()->find('activate=:a',array(':a'=>$a));
    if ($model) {
        $model->status=Users::STATUS_ACTIVE;
        if ($model->update(array('status'))) {
      Yii::app()->user->login(UserIdentity::createAuthenticatedIdentity($model->username,$model->id),0);
      }
      $this->redirect('/');
    } else {
         throw new CHttpException(404, "Invalid Activation Code!");
    }
      }
  }


Two things to note in this chunk of code

1) I’ve used $model->update instead of $model->save as this creates more efficient SQL, rather than saving the whole model, I’ve specified which columns need updating and Yii will create the appropriate UPDATE sql for this.

2) The auto login – this was much more complicated than I had expected. to get this to work correctly, we will have to add a new function to the UserIdentity component to enable creation of a UserIdentity object without a password. Having created the object, we can then pass it to the CWebUser login process as normal.

The UserIdentity Component

So, here we create a new UserIdentity object, without a password and then set the ERROR code to NONE.

I found this solution in the Yii forums (thanks everybody) but also had to add the extra line to set the UserIdentity->_id

/** 
   * Used to create an identity when you know that it has already
   * been authenticated
   * eg: on activation using activation code
   * 
   * @param type $username
   * @return self 
   */
  public static function createAuthenticatedIdentity($username,$id)
  {
    $identity=new self($username,'');
    $identity->errorCode=self::ERROR_NONE;
    $identity->_id=$id;
    return $identity;
  }

So, that brings us to the end of this journey.  I hope you’ve found it useful.  I would love to hear your feedback, so please take a few minutes to add a comment, tweet this URL or something similar.

many thanks

Chris

ps: You can find a fuller example on my Github account: https://github.com/chrisb34/yii-login

Let’s Start a Project!

Contact Me