Login forms in online systems are often easy targets for brute-force attacks; attacks designed to go through all possible values (or at least all probable values) for a password to "guess" a correct one. Securing your forms from such attacks is important, but it can be tricky to do in an effective manner without adversely affecting the user experience of your normal user.

The method I am suggesting in this article is that of queuing login attempts in an effort to limit how many attempts an attacker can execute per second.

Why Queuing?
There are alternative methods commonly used, also designed to prevent brute-forcing, but many of them have problems that make them less than ideal for this purpose. Before I go into the queuing, first let me explain some of the pitfalls of some of those alternatives.

A very frequently seen method is to track the number of login attempts made through a session - or just a cookie - and if too many failed attempts are detected, the client is locked out for some period. The problem with this method is that it relies on the user-agent to maintain a session/cookie. Those can be blocked or deleted very easily, which would bypass the block entirely.

A variation on the above method is to log the IP address rather than using a session or cookie. This solves the unreliability of the cookies, but it introduces other problems. Firstly, IP addresses are not necessarily unique. By blocking an IP, you could in fact be blocking large number of unrelated users. Secondly, IP addresses are easily changeable. Advanced hackers may in fact be using large networks of dummy terminals to hack you, giving them access to a large amount of IP addresses. - In general, any security mechanism that relies on IP tracking is unreliable.

To overcome both the above, some simply remove the reliance on client identification and instead block the users themselves, so that if too many login attempts are made for the same user, regardless of the source of the attempts, the user is locked out for a time. The obvious problem here is that any prankster could easily keep large amounts of users blocked indefinitely by routinely sending a number of invalid login attempts.

Yet another attempt to defy brute-forcing is to slow down the requests themselves, making brute force attacks too slow to be of use. This - in theory - is an good plan, and is in fact the basis of the queuing method I will be demonstrating. However, many implement this rather poorly. We've seen people simply drop a sleep(1); into all login code, making the request take 1 second before it completes. The issue with this approach is that even though you are slowing down the request, you aren't really preventing the hacker from making an obscene amount of attempts. It'll just take one second longer for the results to start piling in. Issuing 50 requests per second won't cause those 50 requests to take 50 seconds, it'll only cause them to take one second + the time it takes each request to execute.

Problems with queuing
So is queueing free of all those problems? No, definitely not. The main problem you may face with a queueing system is DOS attacks. Similar to that of the third method I describe above, a prankster could easily keep the queue full of invalid requests and make normal login requests take intolerably long. An attacker may also try to continually execute several requests per second, which would in no time overload the server with queued login attempts, potentially even crashing it. (Depending on the server config.)

However, there are ways to minimize these risks:

By adding a total queue size you could stop the server from becoming overloaded with login attempts. It would simply drop login requests once the queue has reached a certain size.

By splitting the queue into per-user queues, at least a prankster would not be able to stall all attempts by keeping the queue full, only those meant for specific users. (Unless the number of targeted users allows them to exceeds the overall queue size.)

By only allowing one queue entry per IP address, you would prevent simple attacks from a single source. An attacker would have to use several IP addresses to make any difference. (Not that it would stop anybody determined, but it may be enough to get script kiddies just messing with your queue times to lose interest.) - It's worth noting that, as always, any restrictions based on IP addresses are not exactly reliable, and can cause issues for some users. Organizations, for example, frequently fall under the same network routers or proxies, and all the users within that network will therefore share an external IP. Only one of those users could use the system at a time if an IP restriction like this is put in place. - Consider it very carefully before deciding to add such a restriction.

In the example I will be implementing here, I will demonstrate all three of these measures. Don't take that to mean you should necessarily do so as well!

The Theory
The key to deterring brute-force attacks is to delay each request long enough for the attack to become impractical. If you can only test one password per second, then it'll take forever to test them all. They can still try, but it's a pointless effort. There are just too many possibilities to go through that slowly. However, a normal user will hardly notice if their login attempt is taking a couple of seconds to go through.

So, how do we implement this in PHP? My solution to this is to set up a database where each login attempt is entered and an ID is generated for it. The attempt with the lowest ID will then be allowed to be processed, after which it is removed from the database, and the attempt following it is allowed to proceed. - Ideally I would want there to be a background process that handles the validation; finding the first unprocessed attempt, validating it, updating it's status in the database, and moving on to the next one. Requests for login attempts would add their entries to the database, and then periodically check it to see if their attempt has been processed yet, after which they remove the attempt from the database and return the result to the user.

However, background processes can be troublesome for many PHP hosts, so for the purposes of this article, I'm opting for a solution where each request is responsible for validating their own attempts. They add an entry to the database, then periodically check the database to see if their entry is the first entry listed, then process it, and finally remove it. Simple enough.

Implementation
The first thing we need to do here is set up a database. Because of it's general availability in the world of PHP, I'm going to use MySQL as my database. Note, however, that you may just as well use in-memory systems like Memcache or the APC extension's user cache mechanisms. Those would in fact most likely perform better.

The table I'm going to use will contain four columns:

An ID column that we can use to order the attempts, and find out which attempt is next to be processed.

A last_checked column, which will be updated by each attempt each time the code checks if the attempt is ready. This last_checked column will be used to filter out dead attempts; attempts added by requests that have since been killed off. If we don't take this precaution, any dead request will stall the entire queue until it's manually removed.

An ip_address column, which will store the unsigned integer representation of the client's IP address. This column will have a UNIQUE key restraint on it, to make sure that each IP can only exist once in the queue. (You could just as easily store the IP string, but I have a thing about wasted storage space.)

A username column, to store the name of the user that attempt is waiting for. This will be used to split the queue up into per-user queues. This means that even if there are ten attempts queued up for one user, an attempt for another user will not have to wait.

To manage the entire thing, I've put together a class that can be used to create login attempts, wait for them to be processed, and then handle the validation result.

Note that this code uses the PHP 5.5 password hashing functions. If you do not have PHP 5.5, there are 3rd party libraries that let you easily add this functionality to older versions. (Down to 5.3, with that particular library.)

Usage
To sum up the functionality of the class, we only have two public methods we need to concern ourselves with.

__construct creates the attempt, taking the username, the password and a PDO instance. It sets their respective class attributes to those values, and then triggers the addToQueue function, which goes on to create a new database entry for the class and set the attemptID attribute.

whenReady is our "listener", so to speak. It takes a callable function as the first parameter, and optionally a delay timer value as the second parameter. It will keep calling the isReady function in a loop, each iteration delayed by the value of that second parameter, until it returns TRUE, thus reporting that the attempt is next in line to be processed. Then it will go on to call the the isValid function, which checks the validity of the attempt and removes it from the database. Finally it calls the callback function, and passes the validity value with it as it's only parameter.

Here is an example of how this could be used on a login form's action page:

For PHP 5.2 or lower, the above method of using a closure for the whenReady function is not possible. Instead you would have to define a function, and then pass the name of it as the first parameter to whenReady:

Final Thoughts
If using this method, be mindful of two things. First, making sure the ATTEMPT_DELAY value is appropriate. My value of 1000 ms is just a suggestion. Depending on how busy your server is, you may want to adjust this. - Second, make sure the execution time of the login script is also appropriately set. Requests may need to wait in line for some time, so make sure PHP won't cancel the request before it gets a chance to finish. Don't set it too high either; request lingering open forever isn't a good thing. You need to find a balance that works for you.

Edited, 2013-08-22:
- Added the portion discussing the problems with queuing, and implemented the three suggested anti-DOS prevention methods.

Special thanks to CTphpnwb and adn258 for the discussion that inspired this tutorial, as well as suggesting some of the improvements to it.