From c515e20ea3b81e7e5d5e05ad484360012ae38483 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Fri, 29 Nov 2013 00:28:01 +0000 Subject: [PATCH] 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 --- core/server/models/user.js | 50 ++++++++++++++++++++---- core/test/functional/admin/login_test.js | 26 ++++++++++++ 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/core/server/models/user.js b/core/server/models/user.js index c2354a29a9..bc69951ed6 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -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) { - 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 user; - }, errors.logAndThrowError); + if (user.get('status') !== 'locked') { + return nodefn.call(bcrypt.compare, _userdata.pw, user.get('password')).then(function (matched) { + if (!matched) { + return when(self.setWarning(user)).then(function (remaining) { + s = (remaining > 1) ? 's' : ''; + return when.reject(new Error('Your password is incorrect.
' + + 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; }); diff --git a/core/test/functional/admin/login_test.js b/core/test/functional/admin/login_test.js index 0cd3c5860a..bdd5f01886 100644 --- a/core/test/functional/admin/login_test.js +++ b/core/test/functional/admin/login_test.js @@ -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");