Techweb

How to Limit Wordpress Logins Without a Plugin

One of the main benefits of using a plugin like Wordfence is that it can allow you to limit the number of login attempts within a certain time period - great for avoiding brute force attacks.

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.

This article is filed under: TECHWEB

TAGS