In this mini howto I would like to show how to add a required captcha field in the login form, after a defined number of unsuccessfull attempts. To do this, I will use the blog demo that you have in default Yii download package (path/to/yii/demos/blog).
Basically, you need three things:
in the model, you have to add captcha field as a required field in the rules() method
in the controller, you have to create a different LoginForm model if number of unsuccessfull attempts are greater than N
in the view, you have to show captcha field if number of unsuccessfull attempts are greater than N
In the LoginForm model, you can use 'scenario' to set different required fields, so:
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'),
// add these lines below
array('username,password,verifyCode','required','on'=>'captchaRequired'),
array('verifyCode', 'captcha', 'allowEmpty'=>!CCaptcha::checkRequirements()),
);
}
Moreover, add verifyCode as public property:
public $verifyCode;
In the view, add this code (show captcha field if scenario is set to 'captchaRequired', will see later):
<?php if($model->scenario == 'captchaRequired'): ?>
<div class="row">
<?php echo CHtml::activeLabelEx($model,'verifyCode'); ?>
<div>
<?php $this->widget('CCaptcha'); ?>
<?php echo CHtml::activeTextField($model,'verifyCode'); ?>
</div>
<div class="hint">Please enter the letters as they are shown in the image above.
<br/>Letters are not case-sensitive.</div>
</div>
<?php endif; ?>
Now, the controller. First, add a property to set maximum allowed attempts and a counter that trace failed attempts time to time:
public $attempts = 5; // allowed 5 attempts
public $counter;
then, add a private function that returns true if 'captchaRequired' session value is greater than number of failed attempts.
private function captchaRequired()
{
return Yii::app()->session->itemAt('captchaRequired') >= $this->attempts;
}
We will use this function to know if captcha is required or not. Now, remain to modify actionLogin() method:
public function actionLogin()
{
$model = $this->captchaRequired()? new LoginForm('captchaRequired') : new LoginForm;
// if it is ajax validation request
if(isset($_POST['ajax']) && $_POST['ajax']==='login-form')
{
echo CActiveForm::validate($model);
Yii::app()->end();
}
// collect user input data
if(isset($_POST['LoginForm']))
{
$model->attributes=$_POST['LoginForm'];
// validate user input and redirect to the previous page if valid
if($model->validate() && $model->login())
$this->redirect(Yii::app()->user->returnUrl);
else
{
$this->counter = Yii::app()->session->itemAt('captchaRequired') + 1;
Yii::app()->session->add('captchaRequired',$this->counter);
}
}
// display the login form
$this->render('login',array('model'=>$model));
}
Note that:
- if function captchaRequired() returns true create LoginForm with scenario 'captchaRequired', else create LoginForm with default scenario. This is useful because in protected/models/LoginForm.php we have set two different required fields depending on scenario:
public function rules()
{
return array(
array('username, password', 'required'),
array('username,password,verifyCode','required','on'=>'captchaRequired'),
[... missing code...]
}
- if validation passes redirect to a specific page, but what if validation doesn't pass? In this case we increment the counter, then set a session named 'captchaRequired' with counter value, in this way:
if($model->validate() && $model->login())
$this->redirect(Yii::app()->user->returnUrl);
else
{
$this->counter = Yii::app()->session->itemAt('captchaRequired') + 1;
Yii::app()->session->add('captchaRequired',$this->counter);
}
When 'captchaRequired' session will be equal to maximum allowed attempts (property $attempts) private function captchaRequired() will return true and then LoginForm('captchaRequired') will be created. With scenario set to 'captchaRequired' captcha will be show in the view:
<?php if($model->scenario == 'captchaRequired'): ?>
// code to show captcha
<?php endif; ?>
Easy, uh? ;)
References ¶
http://www.yiiframework.com/forum/index.php/topic/21561-captcha-custom-validation http://drupal.org/node/536274
amazing...
i dont know what to say.. but this is really cool code...
thx zitter..
as you said.. Easy, Uh....
The login attempt counter in the session has severe flaws
The login attempt counter is stored in the session?? An attacker can easily run a brute force process using an empty session each time, right? In this (the most common) scenario this solution is useless. Or I'm missing something?
What you are missing
"Or I'm missing something?"
The only thing you're missing is a chance to write a better wiki to solve those problems :)
thx for the wiki :)
just change the way it store login attempt counter and you're done(save to user log in database or file ), for jpablo if u don't like the way it was written just write your own wiki, or try to change the way you comment :)
Good wiki
This is a good wiki, I was just pointing out a situation that can lead to a false sense of safety. I'll not write another wiki because this one is just fine, I tried to collaborate with a (IMHO) note on security. I'm sorry if anyone took it in the wrong way.
Bug fixes
The following modifications need to be done, otherwise user will be logged in even with a wrong captcha code:
array('verifyCode', 'captcha', 'allowEmpty'=>!CCaptcha::checkRequirements()),
should be changed to
array('verifyCode', 'captcha', 'allowEmpty'=>!CCaptcha::checkRequirements(), 'on'=>'captchaRequired'),
so the check is only ran when it needs to be ran.
The following rule
// password needs to be authenticated array('password', 'authenticate'),
should be last in the rules array so the user is authenticated only if no errors are found so far (that includes having a correct captcha when necessary).
Unsafe
This looks like a simple solution, but be aware that this is totally unsafe and should be avoided. It won't work with a real attack because it does not need a session at all: I am sorry to say that this is useless.
On the other hand, this is nice for logged in users, if you want to prevent from posting several times in little time, for example.
A safer approach (at Php level) would be to write a class that will record failed logins on the db with IPs and timestamps. That would make it easy to determine if a single IP is trying to login too many times in little time, show a captcha first, then lock it for some time, then ban it after many locks, if necessary.
I find it rather inane that some people are saying that using sessions is unsafe and then advocating using the the database to track.
The default action of Yii2 sessions is to store the information in the database. You have to go out of your way to make it insecure.
I suggest that reading https://www.yiiframework.com/doc/guide/2.0/en/runtime-sessions-cookies will clarify the misconceptions being propagated.
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.