a good start would be to just sleep(1);
after a failed login attempt - easy to implement, almost bug-free.
1 second isn't much for a human (especially because login attempts by humans don't fail to often), but 1sec/try brute-force ... sloooow! dictionary attacks may be another problem, but it's in the same domain.
if the attacker starts too may connections to circumvent this, you deal with a kind of DOS-attack. problem solved (but now you've got another problem ^^).
some stuff you should consider:
- if you lock accounts soley on a per IP basis, there may be problems with private networks.
- if you lock accounts soley on a username basis, denial-of-service attacks agains known usernames would be possible
- locking on a IP/username basis (where username is the one attacked) could work better
my suggestion:
complete locking is not desireable (DOS), so a better alternative would be: count the login attempts for a certain username from a unique IP. you could do this with a simple table failed_logins: IP/username/failed_attempts
if the login fails, wait(failed_attempts);
seconds. every xx minutes, run a cron script that decreases failed_logins:failed_attempts
by one.
sorry, i can't provide a premade solution, but this should be trivial to implement.
okay, okay. here's the pseudocode:
<?php
$login_success = tryToLogIn($username, $password);
if (!$login_success) {
// some kind of unique hash
$ipusr = getUserIP() . $username;
DB:update('INSERT INTO failed_logins (ip_usr, failed_attempts) VALUES (:ipusr, 1) ON DUPLICATE KEY UPDATE failed_logins SET failed_attempts = failed_attempts+1 WHERE ip_usr=:ipusr', array((':ipusr' => $ipusr));
$failed_attempts = DB:selectCell('SELECT failed_attempts WHERE ip_usr=:ipusr', array(':ipusr' => $ipusr));
sleep($failed_attempts);
redirect('/login', array('errorMessage' => 'login-fail! ur doin it rong!'));
}
?>
disclaimer: this may not work in certain regions. last thing i heard was that in asia there's a whole country NATed (also, they all know kung-fu).