Yii 1.1: Show captcha after <N> unsuccessfull attempts

17 followers

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

Total 7 comments

#13941 report it
este at 2013/07/08 11:23pm
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.

#9002 report it
Wiseon3 at 2012/07/12 05:01am
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).

#8726 report it
jpablo at 2012/06/22 01:44am
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.

#8725 report it
hening malam at 2012/06/21 11:13pm
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 :)

#8675 report it
zitter at 2012/06/18 07:12pm
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 :)

#8672 report it
jpablo at 2012/06/18 03:31pm
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?

#8649 report it
Peter JK at 2012/06/16 02:31pm
amazing...

i dont know what to say.. but this is really cool code...

thx zitter..

as you said.. Easy, Uh....

Leave a comment

Please to leave your comment.

Write new article