Difference between #1 and #2 of Custom Login Error Messages

unchanged
Title
Custom Login Error Messages
unchanged
Category
Tutorials
unchanged
Tags
custom error, login, LoginForm, UserIdentity, error message, errorCode, addError
changed
Content
Customizing Errors for Login Authentication
------------------

The Yii Framework is very powerful and it provides a lot of functionality right
from the pre-built webapp. One of the nice things that is already established
for you as a developer is the Login authentication. While the default
configuration simply sets it up to run against an array of hard coded usernames
and passwords, the [Yii Blog
Tutorial](http://www.yiiframework.com/doc/blog/1.1/en/prototype.auth "Yii
Blog Tutorial") provides a how to in connecting that login authentication
method to a database so you can run your logins against the registered users.
Talk about sweet deal.

But beyond the tutorial, one of the common things that developers seem to want
to do after customizing the login authentication of CUserIdentity, is to
customize the error messages that are returned from the login portal. It's a
valid desire as many developers want to add a multitude of variables to their
sign in process. A common example is if you use email validation by emailing a
unique activation code. So logically you will run a check of if your user is
active or not before allowing them to log in. But how do we modify the error
message to signify to the user they need to activate that id? 

That's the goal of this tutorial. Let's dive in.

There are actually several key things that need to be adjusted in order to make
a custom error message for the login portal. They are simple but they will drive
you nuts unless you can follow the code to establish just why you get the
behaviour you do. This is something that not every developer is good at,
especially if you don’t have a lot of experience coding in an object oriented
fashion. So this tutorial will do a walkthrough of not only how to change those
error messages, but also how to follow the code so you can figure those things
out as you program.

We’ll start with taking a look at the UserIdentity Component because this is
where it all starts. By default the following code is what you have within the
UserIdenity class found in the Component folder hierarchy.

### UserIdentity Component

~~~
[php]
class UserIdentity extends CUserIdentity
{
	/**
	 * Authenticates a user.
	 * The example implementation makes sure if the username and password
	 * are both 'demo'.
	 * In practical applications, this should be changed to authenticate
	 * against some persistent user identity storage (e.g. database).
	 * @return boolean whether authentication succeeds.
	 */
	public function authenticate()
	{
		$users=array(
			// username => password
			'demo'=>'demo',
			'admin'=>'admin',
		);
		if(!isset($users[$this->username]))
			$this->errorCode=self::ERROR_USERNAME_INVALID;
		else if($users[$this->username]!==$this->password)
			$this->errorCode=self::ERROR_PASSWORD_INVALID;
		else
			$this->errorCode=self::ERROR_NONE;
		return !$this->errorCode;
	}
}
~~~

You can already see how there is multiple errorCode references there. However
when you do a login and say enter the wrong user name (or really an
invalid/unregistered one) you get a generic error code that doesn't really
clarify what the issue is. See below.

![Default Login
Error](http://www.sterlingsavvy.com/tutorials/uploads/login_error_customization/default_login_error.PNG
"Default Login Error")

So how do we fix that? 

Well we have to establish why it's doing it in the first place. That starts by
investigating the class responsible for outputting the form. In this case it
links to the LoginForm. Now we could show the view code of the LoginForm but
understanding MVC architecture, the error code settings will not be defined in
the view. They could be set in the controller (which would be sitecontroller)
but because an error value is just that, a value (thus a property of a class),
there's a good chance this setting is set in the model. So we take a look at the
LoginForm model.

### LoginForm Model

~~~
[php]
class LoginForm extends CFormModel
{
	public $username;
	public $password;
	public $rememberMe;

	private $_identity;

	/**
	 * Declares the validation rules.
	 * The rules state that username and password are required,
	 * and password needs to be authenticated.
	 */
	public function rules()
	{
		return array(
			// username and password are required
			array('username, password', 'required'),
			// rememberMe needs to be a boolean
			array('rememberMe', 'boolean'),
			// password needs to be authenticated
			array('password', 'authenticate'),
		);
	}

	/**
	 * Declares attribute labels.
	 */
	public function attributeLabels()
	{
		return array(
			'rememberMe'=>'Remember me next time',
		);
	}

	/**
	 * Authenticates the password.
	 * This is the 'authenticate' validator as declared in rules().
	 */
	public function authenticate($attribute,$params)
	{
		if(!$this->hasErrors())
		{
			$this->_identity=new UserIdentity($this->username,$this->password);
			if(!$this->_identity->authenticate())
				$this->addError('password','Incorrect username or password.');
		}
	}

	/**
	 * Logs in the user using the given username and password in the model.
	 * @return boolean whether login is successful
	 */
	public function login()
	{
		if($this->_identity===null)
		{
			$this->_identity=new UserIdentity($this->username,$this->password);
			$this->_identity->authenticate();
		}
		if($this->_identity->errorCode===UserIdentity::ERROR_NONE)
		{
			$duration=$this->rememberMe ? 3600*24*30 : 0; // 30 days
			Yii::app()->user->login($this->_identity,$duration);
			return true;
		}
		else
			return false;
	}
}
~~~

If you follow the code you'll find the following in the authenticate function:

~~~
[php]
if(!$this->_identity->authenticate())
	$this->addError('password','Incorrect username or password.');
~~~

There's our error! So how can we customize this? If you try to make changes as
it stands, assuming you don't get any errors you will notice that you can only
change that string there, you cannot add multiple types of errors (which is what
we want). If you try to add multiple types of errors you generally get a
non-responsive login form where it looks like it takes the values but does
nothing. That would be because it's actually spitting out an error to you (if
the login is invalid) but doesn't know how to tell you that because of the code
adjustments made in the authenticate function. 

So why can't we add more than one error? This is because of how the relationship
returns back from the method call being made there. If you take a look at the
login function you see that it actually makes an if case to instantiate the
class of UserIdentity, which is what processes the login. It also calls the
authenticate function of UserIdentity (not to be confused with the authenticate
function used as a validator in LoginForm) to see if the login attempt is
successful. The point of the LoginForm authenticate function then is to
determine if there are any errors. When you follow the code you can establish
that our next step is to revisit the UserIdentity component. 

Before we go, let's look at the code of the if statement in the authenticate
function of LoginForm one more time.

~~~
[php]
if(!$this->_identity->authenticate())
~~~

This is important to notice because what that evaluates as is, "if not
this", implying that the result of _identity->authenticate() is a
boolean (which it is). By looking in the UserIdentity component class we confirm
this.

~~~
[php]
if(!isset($users[$this->username]))
	$this->errorCode=self::ERROR_USERNAME_INVALID;
else if($users[$this->username]!==$this->password)
	$this->errorCode=self::ERROR_PASSWORD_INVALID;
else
	$this->errorCode=self::ERROR_NONE;
return !$this->errorCode;
~~~

What shows there is that if the username or the password is wrong we have an
"error" constant association. But when we return that error constant,
how come we only get 1 error value? It's because we are only passing a true or
false condition. We need to modify this return to make it so it returns the
actual value of the constant. 

~~~
[php]
// need to modify this
return !$this->errorCode;

// to this
return $this->errorCode;
~~~

Now let's go back to the LoginForm and look at our authenticate function there:

~~~
[php]
/**
 * Authenticates the password.
 * This is the 'authenticate' validator as declared in rules().
 */
public function authenticate($attribute,$params)
{
	if(!$this->hasErrors())
	{
		$this->_identity=new UserIdentity($this->username,$this->password);
                        
                // This value right here depends on what we changed!
		if(!$this->_identity->authenticate())
			$this->addError('password','Incorrect username or password.');
	}
}
~~~

### Tracing Back Inheritance

We now have to modify the if statement. It's a change that takes it from
evaluating a boolean to evaluating an actual value. Before we do that however,
we need to know what the values are! This unfortunately requires you to follow
back each case of the inheritance to find out where things like the constant
ERROR_NONE, ERROR_USERNAME_INVALID etc. are. 

Based on where we see ERROR_NONE used we have to check out the UserIdentity
class. When we see no definition of constants (by means of const NAME=VALUE), we
have to look at the inheritance.

~~~
[php]
class UserIdentity extends CUserIdentity
~~~

So UserItentity extends CUserIdentity, which we have to find that class. This
class is part of the Yii framework so we have to investigate the Yii folders
(outside of our webapp) to find what we want. 

CUserIdentity can be found in /framework/web/auth/

When you open up the CUserIdentity.php file you won't find any definition to the
constants but you do find another Extension:

~~~
[php]
class CUserIdentity extends CBaseUserIdentity
~~~

Now lucky for us the CBaseUserIdentity.php resides in the same folder, when we
open that up we at last have finally found our constants:

~~~
[php]
abstract class CBaseUserIdentity extends CComponent implements IUserIdentity
{
	const ERROR_NONE=0;
	const ERROR_USERNAME_INVALID=1;
	const ERROR_PASSWORD_INVALID=2;
	const ERROR_UNKNOWN_IDENTITY=100;

	/**
	 * @var integer the authentication error code. If there is an error, the error
code will be non-zero.
	 * Defaults to 100, meaning unknown identity. Calling {@link authenticate} will
change this value.
	 */
	public $errorCode=self::ERROR_UNKNOWN_IDENTITY;
~~~

Perfect! So now we know what those values are that are associated to the
constants. We also learn that errorCode defaults to 100, implying an Unknown
Identity error. That means we can go back to our LoginForm and fix the
authenticate function.

### Customizing the Errors

~~~
[php]
// We need to change this
public function authenticate($attribute,$params)
{
	if(!$this->hasErrors())
	{
		$this->_identity=new UserIdentity($this->username,$this->password);
		if(!$this->_identity->authenticate())
			$this->addError('password','Incorrect username or password.');
	}
}

// To something like this
public function authenticate($attribute,$params)
{
	if(!$this->hasErrors())
	{
		$this->_identity=new UserIdentity($this->username,$this->password);
		if($this->_identity->authenticate() == 1)
			$this->addError('username','Invalid username.');
		else if($this->_identity->authenticate() == 2)
			$this->addError('password','Incorrect password.');
	}
}
~~~

Now a proper reference would use the constant variable declaration rather than
the value but I choose to show you in the value form so you can corrolate what
you're evaluating (and thus what's being returned from the authenticate function
in UserIdentity). However if you wanted to put in the proper coding it would be
defined just like it is shown in the LoginForm login function:

~~~
[php]
if($this->_identity->errorCode===UserIdentity::ERROR_NONE)
~~~

In our case we would be using UserIdentity::ERROR_USERNAME_INVALID or
UserIdentity::ERROR_PASSWORD_INVALID like so:

~~~
[php]
if($this->_identity->authenticate() ==
UserIdentity::ERROR_USERNAME_INVALID)
	$this->addError('username','Invalid username.');
else if($this->_identity->authenticate() ==
UserIdentity::ERROR_PASSWORD_INVALID)
	$this->addError('password','Incorrect password.');
~~~

**Congratulations you have successfully changed the error messages to have
multiple types of errors defined and displayed! **

Making your own Custom Errors for Login Authentication
------------------

But what about making your own error and thus own error message? That's simple,
now that we have the framework in place to customize and evaluate errors, we
just need to define our own error.

We'll take the Blog Tutorials example of UserIdentity to reference a database
type login authentication and we'll make a custom error for if a user account
"isActive". This of course implies that you have a database field
labeled isActive and that it contains a true or false value (1 or 0) in order to
process.

The Blog Tutorial UserIdentity:

~~~
[php]
<?php
class UserIdentity extends CUserIdentity
{
    private $_id;
 
    public function authenticate()
    {
        $username=strtolower($this->username);
        $user=User::model()->find('LOWER(username)=?',array($username));
        if($user===null)
            $this->errorCode=self::ERROR_USERNAME_INVALID;
        else if(!$user->validatePassword($this->password))
            $this->errorCode=self::ERROR_PASSWORD_INVALID;
        else
        {
            $this->_id=$user->id;
            $this->username=$user->username;
            $this->errorCode=self::ERROR_NONE;
        }
        return $this->errorCode==self::ERROR_NONE;
    }
 
    public function getId()
    {
        return $this->_id;
    }
}
~~~

First we need to change this by defining our own constant. Then we need to
change the logic flow by assessing the error to a value, in our case isActive to
0 (false) or 1 (true). Finally we need to make sure that we change the return
statement to return the value of the error constant not just whether it passes
or not.

~~~
[php]
<?php
class UserIdentity extends CUserIdentity
{
    // Define your Constant(s)
    const ERROR_USERNAME_NOT_ACTIVE = 3;

    private $_id;
 
    public function authenticate()
    {
        $username=strtolower($this->username);
        $user=User::model()->find('LOWER(username)=?',array($username));
        if($user===null)
            $this->errorCode=self::ERROR_USERNAME_INVALID;
        else if(!$user->validatePassword($this->password))
            $this->errorCode=self::ERROR_PASSWORD_INVALID;

        // Add in the logic condition
        else if($user->isActive == 0)
	    $this->errorCode=self::ERROR_USERNAME_NOT_ACTIVE;

        else
        {
            $this->_id=$user->id;
            $this->username=$user->username;
            $this->errorCode=self::ERROR_NONE;
        }

        // Change the return statement to return the value not just a pass
condition
        // was: return $this->errorCode==self::ERROR_NONE;
        return $this->errorCode;
    }
 
    public function getId()
    {
        return $this->_id;
    }
}
~~~

Finally we need to go back to our LoginForm and change / add our custom error.
Remember to switch in the constants as oppose to the numerical values. It's just
good practice.

~~~
[php]
public function authenticate($attribute,$params)
{
	if(!$this->hasErrors())
	{
		$this->_identity=new UserIdentity($this->username,$this->password);
		if($this->_identity->authenticate() == 1)
			$this->addError('username','Invalid username.');
		else if($this->_identity->authenticate() == 2)
			$this->addError('password','Incorrect password.');
		// Your Custom Error :)
                else if($this->_identity->authenticate() == 3)
			$this->addError('username', 'Username is currently not active, please
activate using the activation URL in your email and try again.');
	}
}
~~~

The final result is:

![Custom
Error](http://www.sterlingsavvy.com/tutorials/uploads/login_error_customization/custom_error.PNG
"Custom Error")

One final thing to note is that the addError pulls an attribute value first,
here you see 'username' or 'password'. These reference to which field to show
the error under. In our case with our custom value we know the user
authenticates properly and exist in our DB, they just haven't activated their
account yet. As such we label the error to the 'username'.

That's it for the tutorial, I hope you learned a lot in understanding how to
follow the logical flow of code assignments and debugging the functions,
variables and classes in this case. I also hope this helps you put to good use
how to modify your own custom errors into the login authentication process.

Thanks for reading!
~ Whoopass