HomePHP FrameworkSecure Yii2 Login from Brute Force with CAPTCHA

Secure Yii2 Login from Brute Force with CAPTCHA

Advertisement

Brute force password guessing remains a real threat to website security. This tutorial provides a step-by-step guide to securing the login form in the Yii2 framework by automatically enabling CAPTCHA validation after several failed login attempts, thereby adding a robust layer of defense to your application.

Advertisement

Why is CAPTCHA Crucial for Login Forms?

Brute force attacks are an old yet persistently effective method frequently used by hackers. They employ bots to automatically try thousands of username and password combinations. An unprotected login form can lead to compromised admin accounts and stolen sensitive data.

  • Blocks Bots: CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) is specifically designed to distinguish humans from robots.
  • Increases Attack Difficulty: Triggering a CAPTCHA after several failed login attempts significantly raises the effort required by an attacker.
  • Defense-in-Depth Strategy: CAPTCHA is a vital additional security layer, complementing strong passwords and login attempt throttling.

Implementing CAPTCHA conditionally (only when needed) maintains a good user experience for legitimate users while creating a barrier for bots and attackers.

Step 1: Modify the LoginForm.php Model

First, add a property for the CAPTCHA and its validation logic to the `LoginForm` model. The key is to make the CAPTCHA mandatory only after a certain number of failed login attempts.

Advertisement
<?php

namespace app\models;

use Yii;
use yii\base\Model;

class LoginForm extends Model
{
    public $username;
    public $password;
    public $rememberMe = true;
    public $verifyCode; // New property for CAPTCHA

    // ... other existing code ...

    public function rules()
    {
        return [
            // Rules for username and password
            [['username', 'password'], 'required'],
            ['rememberMe', 'boolean'],
            ['password', 'validatePassword'],

            // CAPTCHA RULES: Required only if loginFailed is true
            ['verifyCode', 'required', 'when' => function($model) {
                return $model->loginFailed;
            }, 'message' => 'CAPTCHA code is required after multiple failed attempts.'],
            ['verifyCode', 'captcha', 'when' => function($model) {
                return $model->loginFailed;
            }],
        ];
    }

    public function validatePassword($attribute, $params)
    {
        if (!$this->hasErrors()) {
            $user = $this->getUser();
            if (!$user || !$user->validatePassword($this->password)) {
                // KEY LOGIC: Increment the failed attempt counter
                $attempts = Yii::$app->session->get('_loginAttempts', 0) + 1;
                Yii::$app->session->set('_loginAttempts', $attempts);

                $this->addError($attribute, 'Incorrect username or password.');
            } else {
                // Reset counter on successful login
                Yii::$app->session->remove('_loginAttempts');
            }
        }
    }

    // Getter to check the failure status
    public function getLoginFailed()
    {
        // CAPTCHA triggers after 3 or more failures
        return Yii::$app->session->get('_loginAttempts', 0) >= 3;
    }

    // ... getUser() method and others ...
}

Key Code Explanation:

  • The $verifyCode property holds the user’s CAPTCHA input.
  • The getLoginFailed() method checks the _loginAttempts session variable. It returns true if failed attempts are ≥ 3.
  • The 'when' parameter in the validation rules ensures the verifyCode rules apply only when loginFailed is true.
  • The failed attempt counter in the session increments with each password validation failure. A successful login resets it.

Step 2: Display CAPTCHA in the Login.php View

Next, modify the login view file to render the CAPTCHA widget only when the $model->loginFailed condition is met.

Advertisement
<?php

use yii\helpers\Html;
use yii\bootstrap5\ActiveForm; // or yii\widgets\ActiveForm

/* @var $this yii\web\View */
/* @var $form yii\widgets\ActiveForm */
/* @var $model app\models\LoginForm */

$this->title = 'Login';
?>
<div class="site-login">
    <h1><?= Html::encode($this->title) ?></h1>

    <div class="row">
        <div class="col-lg-5">
            <?php $form = ActiveForm::begin(['id' => 'login-form']); ?>

                <?= $form->field($model, 'username')->textInput(['autofocus' => true]) ?>

                <?= $form->field($model, 'password')->passwordInput() ?>

                <?= $form->field($model, 'rememberMe')->checkbox() ?>

                <!-- CONDITIONAL CAPTCHA SECTION -->
                <?php if ($model->loginFailed): ?>
                    <div class="alert alert-warning">
                        Too many failed login attempts. Please verify you are human.
                    </div>
                    <?= $form->field($model, 'verifyCode')->widget(\yii\captcha\Captcha::className()) ?>
                <?php endif; ?>

                <div class="form-group">
                    <?= Html::submitButton('Login', ['class' => 'btn btn-primary', 'name' => 'login-button']) ?>
                </div>

            <?php ActiveForm::end(); ?>
        </div>
    </div>
</div>

Step 3: Configure the CAPTCHA Action in the Controller

The CAPTCHA widget requires a dedicated controller action to generate the image and validate the code. Ensure the ‘captcha’ action is configured in `SiteController`.

<?php

namespace app\controllers;

use yii\web\Controller;

class SiteController extends Controller
{
    // ... other actions() ...

    public function actions()
    {
        return [
            'error' => ['class' => 'yii\web\ErrorAction'],
            // THE CAPTCHA ACTION MUST BE PRESENT
            'captcha' => [
                'class' => 'yii\captcha\CaptchaAction',
                'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, // Important for testing
                'minLength' => 4, // Minimum code length
                'maxLength' => 5, // Maximum code length
                'offset' => 2,    // Visual difficulty
                'testLimit' => 1, // How many times a code can be used (1 = one-time use)
            ],
            // ... other actions ...
        ];
    }

    // ... actionLogin() and other methods ...
}

Parameters like minLength, maxLength, and testLimit allow you to fine-tune the CAPTCHA’s security and usability for your application .

Important Notes & Additional Security

  • Session-Based: This implementation relies on PHP sessions. Ensure your server’s session configuration is secure.
  • Not the Only Solution: CAPTCHA is an excellent defensive layer, but you should combine it with other measures such as:
    1. Login Rate Limiting: Use components like `yiifiltersRateLimiter` to restrict the number of login requests from a single IP address within a timeframe .
    2. Strong Passwords: Enforce complex passwords for all users, especially administrators.
    3. Logging & Monitoring: Record all login attempts, both successful and failed, for further analysis.
  • Counter Reset: In the code above, the counter resets on successful login. You could also implement a time-based reset (e.g., after 30 minutes) for better user experience.

By implementing this conditional CAPTCHA, you add a vital security layer that intelligently disrupts the automated flow of brute force attacks, while maintaining ease of use for legitimate users who simply mistype their password once.

Latest Articles