Adds login limiter

Closes #499
* On wrong passwords, statuses: `active` -> `warn-1` -> `warn-2` -> `warn-3` -> `locked`
* On login check, if user's status is `locked`, login automatically fails and user is encouraged to reset password. Does not even bother to check for passwords.
* login attempts tell user how many attempts she has remaining in notification box
* successful login will reset status to `active`
* resetting password with forgotten password emailed token resets status to `active`
* complete with a test suite
This commit is contained in:
Gabor Javorszky 2013-11-29 00:28:01 +00:00
parent e4a5356a69
commit c515e20ea3
2 changed files with 69 additions and 7 deletions

View File

@ -155,17 +155,53 @@ User = ghostBookshelf.Model.extend({
},
setWarning: function (user) {
var status = user.get('status'),
regexp = /warn-(\d+)/i,
level;
if (status === 'active') {
user.set('status', 'warn-1');
level = 1;
} else {
level = parseInt(status.match(regexp)[1], 10) + 1;
if (level > 3) {
user.set('status', 'locked');
} else {
user.set('status', 'warn-' + level);
}
}
return when(user.save()).then(function () {
return 5 - level;
});
},
// Finds the user by email, and checks the password
check: function (_userdata) {
var self = this,
s;
return this.forge({
email: _userdata.email.toLocaleLowerCase()
}).fetch({require: true}).then(function (user) {
if (user.get('status') !== 'locked') {
return nodefn.call(bcrypt.compare, _userdata.pw, user.get('password')).then(function (matched) {
if (!matched) {
return when.reject(new Error('Your password is incorrect'));
return when(self.setWarning(user)).then(function (remaining) {
s = (remaining > 1) ? 's' : '';
return when.reject(new Error('Your password is incorrect.<br>' +
remaining + ' attempt' + s + ' remaining!'));
});
}
return when(user.set('status', 'active').save()).then(function (user) {
return user;
});
}, errors.logAndThrowError);
}
return when.reject(new Error('Your account is locked due to too many ' +
'login attempts. Please reset your password to log in again by clicking ' +
'the "Forgotten password?" link!'));
}, function (error) {
/*jslint unparam:true*/
return when.reject(new Error('There is no user with that email address.'));
@ -285,7 +321,7 @@ User = ghostBookshelf.Model.extend({
var foundUser = results[0],
passwordHash = results[1];
foundUser.save({password: passwordHash});
foundUser.save({password: passwordHash, status: 'active'});
return foundUser;
});

View File

@ -74,6 +74,32 @@ CasperTest.begin("Can't spam it", 3, function suite(test) {
casper.wait(2000);
}, true);
CasperTest.begin("Login limit is in place", 3, function suite(test) {
casper.thenOpen(url + "ghost/signin/", function testTitle() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
casper.waitForOpaque(".login-box",
function then() {
this.fill("#login", falseUser, true);
},
function onTimeout() {
test.fail('Sign in form didn\'t fade in.');
});
casper.wait(2100, function doneWait() {
this.fill("#login", falseUser, true);
});
casper.waitForText('remaining', function onSuccess() {
test.assert(true, 'The login limit is in place.');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
}, function onTimeout() {
test.assert(false, 'We did not trip the login limit.');
});
}, true);
CasperTest.begin("Can login to Ghost", 4, function suite(test) {
casper.thenOpen(url + "ghost/login/", function testTitle() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");