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 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 ¶
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.
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 ¶
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:
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.
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.
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.
// 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:
/**
* 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.
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:
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:
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 ¶
// 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:
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:
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
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
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.
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:
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
Get error Code
Hi, thx for wiki
I think for get error code in LoginForm.php we must use
public function authenticate($attribute,$params) { if(!$this->hasErrors()) { $this->_identity=new UserIdentity($this->username,$this->password); if(!$this->_identity->authenticate()) { if(($this->_identity->errorCode == 1) or ($this->_identity->errorCode == 2)) $this->addError('password',Yii::t('zii','Invalid Username or Password')); elseif($this->_identity->errorCode ==-1) $this->addError('username',Yii::t('zii','Username is currently not active')); else $this->addError('username',Yii::t('zii','Invalid Exception')); } } }
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.