Securing web logins against dictionary attacks

It's good to learn from your mistakes; it's even better to learn from other people's. So, when Twitter got hacked by a simple dictionary attack, we, the development team at Compsoft, thought it a good time to review our password security model.

Because pretty much everyone wants password protection for at least some part of their web site, password security is something we've implemented quite a few times already. And as nobody enjoys re-inventing the wheel, this already lives in our well-established reusable code libraries - meaning that if we ever identify a weakness we can fix it in one place. At the very least, all our future customers get this improvement out of the box; where appropriate, the upgrades get rolled out to our existing customers too.
Our password handling was already pretty tight - we use some pretty aggressive salting and hashing to ensure that passwords are held so securely that even we can't crack them. Even though I have full access to the database, I don't know your password; I can't tell anything about your password; I can't even tell if two people have the same password. If I copy the password details from my user record to yours, I still won't be able to log in as you. It's got to the point that it's quite difficult for us to set up the first user record in a new database - even copying password details from one database to another won't work.
Anyway, storing the password securely is all well and good, but doesn't help if your users use passwords that are easily guessable, as Twitter found to their embarrassment. The key feature they'd failed to implement (and that we realised we'd immediately have to add to our arsenal) was preventing unlimited password attempts.
Though dictionary attacks are always going to be slow when executed across the internet, they're still entirely possible - especially given that the only limit on a typical hacker's time is how quickly he gets bored.
The easiest solution is to say "if you get your password wrong five times, your account gets blocked". This is quick and easy to implement, but what does it mean for our users? Sure, they'd be protected against having their account hacked, but could they not legitimately take five attempts before they remember which password they used for this site? At times I've taken a lot more than that, going through more and more possible passwords that I think I might have used, before finding that the one I tried first was right - I'd just typed it wrong.
Increasing the number of attempts allowed doesn't really solve the problem - if they breach it, they're locked out completely. That means we'd have to add some mechanism whereby they can contact an administrator and convince them that they are who they say they are, so that the admin will re-enable their account. Not only would this alienate users, but this isn't really practical for a solution we can reuse across many intra- and internet sites.
What's worse, it's easy for one malicious user to lock other users out of their accounts, simply by repeatedly trying to log in as them.
We need something that is entirely invisible to as many genuine users as possible, but that is insurmountable to a dictionary attack in any reasonable amount of time. Also, should we start suspecting a legitimate user might be a hacker, they mustn't feel like we've just slammed the door in their face.
The solution we finally opted for goes like this:
  • You get ten attempts to try to remember your password.
  • On the eleventh attempt, your account is locked for one minute.
  • Each subsequent attempt locks your account for twice as long: two minutes, four minutes, eight minutes...
  • If you make no log in attempts for twenty-four hours, we reset the counter to zero again.
Almost all users will never see that this security exists. Those who break the ten-guess limit see a polite message informing them of why we think something's wrong ("a large number of login attempts using the wrong password") and how long they new have left to wait before they can try again.
The first few times they see this limit they'll not have to wait long, and in the meantime we're gently teaching them to think a little more carefully about what password they might have used.
On the other hand, a hacker using a dictionary attack would have to attack the same user id persistently for a full 24 hours to get through just the first 21 words in his attack dictionary.
The experience of the attacked user isn't too bad either - they'll only know there's even been a problem if they try to log in shortly after the hacker (or malicious user) has been trying to access their account. The worst that could happen would be that they wouldn't be able to log in until tomorrow.
For us, this is a nice solution. We've implemented it as part of our reusable code framework, and included it in the template used by our code generation tool. This means that even though we haven't started work making your next website for you yet, your next website is already more secure than Twitter was.