How to Limit Wordpress Logins Without A Plugin
No Plugin Required?
I find it unusual that this facility is still not provided by default, so I wondered how easy it would be to do this without the need to install a plug-in.
It turns out that it is quite straightforward if we add a couple of small functions to our theme's functions.php file. We can hook these into the Wordpress authentication process to check for failed attempts and then disable the login function for a certain time period.
We can store both the number of attempts and the time period within a Wordpress Transient, which was actually something new to me before I researched this.
- Transients are very similar to WordPress options, except transients have a designated lifespan making them a bit more like browser cookies
- They are stored in the options table with the prefix
_transient_
- Wordpress creates another transient of the same name but prefixed
_transient_timeout_
that contains the expiry timestamp - Transients will be deleted by Wordpress after their allocated expiry time, or you can delete them yourself in code
- Because of this, transients should not be used for storing important data that doesn't exist elsewhere
Full Code
Here is the complete code we need to achieve our login limiter - we'l break it down in a second:
// LIMIT LOGIN ATTEMPTS
const MAX_LOGIN_TRIES = 3;
const LOGIN_DELAY = 5; // in minutes
const THE_DELAY = (LOGIN_DELAY * MINUTE_IN_SECONDS);
function check_attempted_login($user, $username, $password) {
if (get_transient('login_lock')) {
$loginHistory = get_transient('login_lock');
if ($loginHistory['tries'] >= MAX_LOGIN_TRIES) {
$until = get_option('_transient_timeout_login_lock');
$remain = time_remain($until);
return new WP_Error('too_many_tried', sprintf(__('ERROR: You have reached the maximum allowed login attempts (%1$s) - you will be able to try again in %2$s.'), MAX_LOGIN_TRIES, $remain));
}
}
return $user;
}
add_filter('authenticate', 'check_attempted_login', 30, 3);
function login_failed() {
echo "FAILED";
if (get_transient('login_lock')) {
$loginHistory = get_transient('login_lock');
$loginHistory['tries'] ++;
if ($loginHistory['tries'] <= MAX_LOGIN_TRIES) {
set_transient('login_lock', $loginHistory, THE_DELAY);
}
}
else {
// first time failed to login
$loginHistory = array('tries' => 1);
set_transient('login_lock', $loginHistory, THE_DELAY);
}
if ($loginHistory['tries'] == MAX_LOGIN_TRIES) header("Refresh:0");
}
add_action('wp_login_failed', 'login_failed');
function login_success($user_login, $user) {
if (get_transient('login_lock')) delete_transient('login_lock');
}
add_action('wp_login', 'login_success', 10, 2);
function time_remain($timestamp) {
$now = time();
$difference = abs($now - $timestamp);
$output = (($difference < 59) ? $difference . " second" : round(($difference/60)) . " minute")
. ($difference <= 1 || ($difference > 59 && round($difference/60) == 1) ? "" : "s");
return $output;
}
Setting Up
Firstly, we define the number of attempts allowed before lockout, and the lockout time period. Note that we are using some time constants that Wordpress helpfully provides (full list in the Codex)
Checking the Attempts
the check_attempted_login
function has been hooked into the WP authenticate process - checking username and password input.
If our transient is already set then we have failed at least one login attempt. We check to see if the stored attempt value has been reached or exceeded. If so then provide feedback to the user and give a meaningful expiry time (using the time_remain
function).
Setting the Attempts
the login_failed
function will run when a user has entered the wrong details. It first checks to see if the transient is already set, and if not then creates it.
If it already exists, then we retrieve it and increment it before saving it again. Note that it will retain the original expiry time set at the first failed attempt. We need to update the _transient_timeout_
to change this.
Note also that we are forcing a page refresh if we have just made our final attempt.
I added this here because even though we've saved the maximum login attempts in the transient, users will only see the locked out error message AFTER the next run of authenticate - which in our example of 3 maximum attempts will mean making a 4th (and therefore redundant) login attempt.
A Bit of Cleanup
Finally, we are deleting the transient on a successful login to reset things. If we didn't do this, then the following situation could occur:
- Lockout attempts set to 3 times and lockout time set to 10 minutes
- User makes 2 failed login attempts but gets in on the third
- They log out before the 10 minute expiry and then want to log in again
- They will only have one attempt to login correctly because the transient with two failed attempts is still valid
Of course you want to be strict and force 3 attempts within any 10 minute period then simply remove this function.
How to get Around a Lockout
So how do we unlock a Wordpress site that we've locked with this code?
Assuming we have access to the backend via cPanel and/or FTP then there are a couple of possible options:
- Remove the code from the functions file and re-upload via FTP or cPanel's file manager
- Use PHPMyAdmin to go into the options table in your WP database, find the entry
transient_login_lock
and delete it.
In either case, refreshing the login page should hopefully provide us with the opportunity to log in again.