I have done that, although the code is quite old now, so I won’t share the specifics.
There’s a field in the User model which tracks successive failed logins. It’s incremented if login fails and reset to zero if login succeeds.
There are config values which specify how many failed login attempts will trigger the display and requirement of a captcha image and how many failed login attempts will lock the account. It can only be unlocked by the user requesting a verification email or by an administrator.
Once the account is locked, the failed login field is no longer incremented; the user just receives a message stating that the account is locked, with a link to request an unlock email.
public function rules()
{
return array(
...
array('email', 'checkRecaptchaRequired'),
array('recaptcha', 'checkRecaptcha'),
...
);
}
// Only checks if recaptcha required and was requested by system. Does not increment failed logins.
// Sets error if required but not requested in form.
public function checkRecaptchaRequired($attribute, $params)
{
if (!$this->hasErrors())
{
if ($this->_userIdentity === NULL)
die('Internal error in validation.');
$userRecord = $this->_userIdentity->getUserRecord();
$max_attempts = Yii::app()->params['login']['attemptsUntilRecaptcha'];
// Error if account required image validation but not requested by form.
if ($userRecord && $userRecord->failed_login_attempts >= $max_attempts && $this->recaptcha === NULL)
{
$lastLoginText = $userRecord->last_successful_login === NULL ? 'your account was created' : 'your last successful login';
$this->addError('recaptcha', 'Image verification is required.');
$this->formMessage .= '<p>Your account now requires image verification as a result of ' . $max_attempts .
' or more failed login attempts since ' . $lastLoginText .
'. The details you provided in this attempt have not been checked, and have not been counted ' .
' as a failed login attempt. Please provide the text from the image and resubmit.';
}
}
}
public function checkRecaptcha($attribute, $params)
{
// Only check if valid user and image shown in form
if ($this->_userIdentity !== NULL && $this->recaptcha !== NULL)
{
$userRecord = $this->_userIdentity->getUserRecord();
if ($userRecord && $userRecord->failed_login_attempts >= Yii::app()->params['login']['attemptsUntilRecaptcha'])
{
Yii::import('ext.recaptcha.EReCaptchaCheck');
if (!EReCaptchaCheck::check())
{
$this->addError($attribute, EReCaptchaCheck::$message);
$this->_doIncrementFailedLogins = TRUE;
}
}
}
}
// Returns boolean. Called after all validation and failed login incrementation.
public function displayRecaptcha()
{
if ($this->_userIdentity !== NULL)
{
$userRecord = $this->_userIdentity->getUserRecord();
if ($userRecord)
{
return $userRecord->failed_login_attempts >= Yii::app()->params['login']['attemptsUntilRecaptcha'];
}
}
return FALSE;
}
Again, this is old code. I would probably handle it differently now.