Show captcha after <N> unsuccessfull attempts

18 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 6 comments

#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