0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Merge pull request #3310 from sebgie/issue#3128-2

Fix tests failing because of spam protection
This commit is contained in:
Hannah Wolfe 2014-07-17 16:41:31 +01:00
commit efa7665124
15 changed files with 162 additions and 123 deletions

View file

@ -127,8 +127,17 @@ authentication = {
},
isSetup: function () {
return dataProvider.User.findOne({status: 'active'}).then(function (user) {
if (user) {
return dataProvider.User.query(function (qb) {
qb.where('status', '=', 'active')
.orWhere('status', '=', 'warn-1')
.orWhere('status', '=', 'warn-2')
.orWhere('status', '=', 'warn-3')
.orWhere('status', '=', 'warn-4')
.orWhere('status', '=', 'locked');
}).fetch().then(function (users) {
if (users) {
return when.resolve({ setup: [{status: true}]});
} else {
return when.resolve({ setup: [{status: false}]});

View file

@ -245,7 +245,15 @@ errors = {
}
errors.renderErrorPage(err.status || 500, err, req, res, next);
} else {
res.send(err.status || 500, err);
// generate a valid JSON response
var statusCode = 500,
errorContent = {};
statusCode = err.code || 500;
errorContent.message = _.isString(err) ? err : (_.isObject(err) ? err.message : 'Unknown Error');
errorContent.type = err.type || 'InternalServerError';
res.json(statusCode, errorContent);
}
}
};

View file

@ -9,11 +9,13 @@ var _ = require('lodash'),
path = require('path'),
api = require('../api'),
passport = require('passport'),
errors = require('../errors'),
expressServer,
oauthServer,
ONE_HOUR_MS = 60 * 60 * 1000,
ONE_YEAR_MS = 365 * 24 * ONE_HOUR_MS;
ONE_YEAR_MS = 365 * 24 * ONE_HOUR_MS,
loginSecurity = [];
function isBlackListedFileType(file) {
var blackListedFileTypes = ['.hbs', '.md', '.json'],
@ -149,6 +151,31 @@ var middleware = {
});
},
// ### Spam prevention Middleware
// limit signin requests to one every two seconds
spamPrevention: function (req, res, next) {
var currentTime = process.hrtime()[0],
remoteAddress = req.connection.remoteAddress,
denied = '';
// filter for IPs that tried to login in the last 2 sec
loginSecurity = _.filter(loginSecurity, function (logTime) {
return (logTime.time + 2 > currentTime);
});
// check if IP tried to login in the last 2 sec
denied = _.find(loginSecurity, function (logTime) {
return (logTime.ip === remoteAddress);
});
if (!denied || expressServer.get('disableLoginLimiter') === true) {
loginSecurity.push({ip: remoteAddress, time: currentTime});
next();
} else {
return next(new errors.UnauthorizedError('Slow down, there are way too many login attempts!'));
}
},
// work around to handle missing client_secret
// oauth2orize needs it, but untrusted clients don't have it
addClientSecret: function (req, res, next) {
@ -157,9 +184,15 @@ var middleware = {
}
next();
},
// ### Authenticate Client Middleware
// authenticate client that is asking for an access token
authenticateClient: function (req, res, next) {
return passport.authenticate(['oauth2-client-password'], { session: false })(req, res, next);
},
// ### Generate access token Middleware
// register the oauth2orize middleware for password and refresh token grants
generateAccessToken: function (req, res, next) {
return oauthServer.token()(req, res, next);
},

View file

@ -43,8 +43,8 @@ oauth = {
}).catch(function () {
return done(null, false);
});
}).catch(function () {
return done(null, false);
}).catch(function (error) {
return done(error);
});
});
}));

View file

@ -270,15 +270,16 @@ User = ghostBookshelf.Model.extend({
var self = this,
s;
return this.getByEmail(object.email).then(function (user) {
if (!user || user.get('status') === 'invited' || user.get('status') === 'inactive') {
return when.reject(new Error('NotFound'));
if (!user || user.get('status') === 'invited' || user.get('status') === 'invited-pending'
|| user.get('status') === 'inactive') {
return when.reject(new errors.NotFoundError('NotFound'));
}
if (user.get('status') !== 'locked') {
return nodefn.call(bcrypt.compare, object.password, 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.<br>' +
return when.reject(new errors.UnauthorizedError('Your password is incorrect.<br>' +
remaining + ' attempt' + s + ' remaining!'));
});
}
@ -288,7 +289,7 @@ User = ghostBookshelf.Model.extend({
});
}, errors.logAndThrowError);
}
return when.reject(new Error('Your account is locked due to too many ' +
return when.reject(new errors.NoPermissionError('Your account is locked due to too many ' +
'login attempts. Please reset your password to log in again by clicking ' +
'the "Forgotten password?" link!'));

View file

@ -73,6 +73,7 @@ apiRoutes = function (middleware) {
router.post('/authentication/setup', api.http(api.authentication.setup));
router.get('/authentication/setup', api.http(api.authentication.isSetup));
router.post('/authentication/token',
middleware.spamPrevention,
middleware.addClientSecret,
middleware.authenticateClient,
middleware.generateAccessToken

View file

@ -3,38 +3,8 @@
/*globals CasperTest, casper, url, newUser, user, falseUser */
// TODO fix signup vs setup testing
//CasperTest.begin('Ensure a User is Registered', 3, function suite(test) {
// casper.thenOpenAndWaitForPageLoad('signup', function checkUrl() {
// test.assertUrlMatch(/ghost\/signup\/$/, 'Landed on the correct URL');
// });
//
// casper.waitForOpaque('.signup-box',
// function then() {
// this.fillAndSave('#signup', newUser);
// },
// function onTimeout() {
// test.fail('Sign up form didn\'t fade in.');
// });
//
// casper.captureScreenshot('login_register_test.png');
//
// casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
// test.assertSelectorHasText('.notification-error', 'already registered');
// // If the previous assert succeeds, then we should skip the next check and just pass.
// casper.echoConcise('Already registered!');
// casper.captureScreenshot('already_registered.png');
// }, function onTimeout() {
// test.assertUrlMatch(/ghost\/\d+\/$/, 'If we\'re not already registered, we should be logged in.');
// casper.echoConcise('Successfully registered.');
// }, 2000);
//
// casper.thenOpenAndWaitForPageLoad('signout', function then() {
// test.assertUrlMatch(/ghost\/signin/, 'We got redirected to signin page.');
// });
//}, true);
CasperTest.begin('Ghost admin will load login page', 3, function suite(test) {
CasperTest.Routines.signout.run(test);
casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/signin\/$/, 'We should be presented with the signin page.');
@ -48,84 +18,90 @@ CasperTest.begin('Ghost admin will load login page', 3, function suite(test) {
test.assert(link === '/ghost/forgotten/', 'Has correct forgotten password link');
});
});
}, true);
});
// Note, this test applies to a global redirect, which sends us to the standard admin.
// Once Ember becomes the standard admin, this test should still pass.
CasperTest.begin('Redirects login to signin', 2, function suite(test) {
CasperTest.Routines.signout.run(test);
casper.start(url + 'ghost/login/', function testRedirect(response) {
test.assertEqual(response.status, 200, 'Response status should be 200.');
test.assertUrlMatch(/ghost\/signin\//, 'Should be redirected to /signin/.');
});
}, true);
// TODO: please uncomment when the spam prevention bug is fixed (https://github.com/TryGhost/Ghost/issues/3128)
// CasperTest.begin('Can\'t spam it', 4, function suite(test) {
// casper.thenOpenAndWaitForPageLoad('signin', function testTitle() {
// test.assertTitle('Ghost Admin', 'Ghost admin has no title');
// test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
// });
// casper.waitForOpaque('.login-box',
// function then() {
// this.fillAndSave('#login', falseUser);
// },
// function onTimeout() {
// test.fail('Sign in form didn\'t fade in.');
// });
});
// casper.captureScreenshot('login_spam_test.png');
CasperTest.begin('Can\'t spam it', 4, function suite(test) {
CasperTest.Routines.signout.run(test);
// casper.waitForText('attempts remaining!', function then() {
// this.fillAndSave('#login', falseUser);
// });
casper.thenOpenAndWaitForPageLoad('signin', function testTitle() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
});
// casper.captureScreenshot('login_spam_test2.png');
casper.waitForOpaque('.login-box',
function then() {
this.fillAndSave('#login', falseUser);
},
function onTimeout() {
test.fail('Sign in form didn\'t fade in.');
});
// casper.waitForText('Slow down, there are way too many login attempts!', function onSuccess() {
// test.assert(true, 'Spamming the login did result in an error notification');
// test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
// }, function onTimeout() {
// test.assert(false, 'Spamming the login did not result in an error notification');
// });
// // This test causes the spam notification
// // add a wait to ensure future tests don't get tripped up by this.
// casper.wait(2000);
// }, true);
casper.captureScreenshot('login_spam_test.png');
// TODO: please uncomment when the spam prevention bug is fixed (https://github.com/TryGhost/Ghost/issues/3128)
// CasperTest.begin('Login limit is in place', 4, function suite(test) {
// casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() {
// test.assertTitle('Ghost Admin', 'Ghost admin has no title');
// test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
// });
casper.waitForText('attempts remaining!', function then() {
this.fillAndSave('#login', falseUser);
});
// casper.waitForOpaque('.login-box',
// function then() {
// this.fillAndSave('#login', falseUser);
// },
// function onTimeout() {
// test.fail('Sign in form didn\'t fade in.');
// });
casper.captureScreenshot('login_spam_test2.png');
// casper.wait(2100, function doneWait() {
// this.fillAndSave('#login', falseUser);
// });
casper.waitForText('Slow down, there are way too many login attempts!', function onSuccess() {
test.assert(true, 'Spamming the login did result in an error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
}, function onTimeout() {
test.assert(false, 'Spamming the login did not result in an error notification');
});
// 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.');
// });
// // This test used login, add a wait to
// // ensure future tests don't get tripped up by this.
// casper.wait(2000);
// }, true);
// This test causes the spam notification
// add a wait to ensure future tests don't get tripped up by this.
casper.wait(2000);
});
CasperTest.begin('Login limit is in place', 4, function suite(test) {
CasperTest.Routines.signout.run(test);
casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
});
casper.waitForOpaque('.login-box',
function then() {
this.fillAndSave('#login', falseUser);
},
function onTimeout() {
test.fail('Sign in form didn\'t fade in.');
});
casper.wait(2100, function doneWait() {
this.fillAndSave('#login', falseUser);
});
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.');
});
// This test used login, add a wait to
// ensure future tests don't get tripped up by this.
casper.wait(2000);
});
CasperTest.begin('Can login to Ghost', 5, function suite(test) {
CasperTest.Routines.signout.run(test);
casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
@ -144,9 +120,11 @@ CasperTest.begin('Can login to Ghost', 5, function suite(test) {
}, function onTimeOut() {
test.fail('Failed to signin');
});
}, true);
});
CasperTest.begin('Authenticated user is redirected', 8, function suite(test) {
CasperTest.Routines.signout.run(test);
casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
@ -173,29 +151,31 @@ CasperTest.begin('Authenticated user is redirected', 8, function suite(test) {
}, function onTimeOut() {
test.fail('Failed to redirect');
});
}, true);
});
// TODO: please uncomment when the validation problem is fixed (https://github.com/TryGhost/Ghost/issues/3120)
// CasperTest.begin('Ensure email field form validation', 3, function suite(test) {
// casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() {
// test.assertTitle('Ghost Admin', 'Ghost admin has no title');
// test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
// });
// casper.waitForOpaque('.js-login-box',
// function then() {
// this.fillAndSave('form.login-form', {
// 'email': 'notanemail'
// });
// },
// function onTimeout() {
// test.fail('Login form didn\'t fade in.');
// });
CasperTest.begin('Ensure email field form validation', 3, function suite(test) {
CasperTest.Routines.signout.run(test);
// casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
// test.assertSelectorHasText('.notification-error', 'Invalid Email');
// }, function onTimeout() {
// test.fail('Email validation error did not appear');
// }, 2000);
casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
});
// }, true);
casper.waitForOpaque('.js-login-box',
function then() {
this.fillAndSave('form.login-form', {
'identification': 'notanemail'
});
},
function onTimeout() {
test.fail('Login form didn\'t fade in.');
});
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'Invalid Email');
}, function onTimeout() {
test.fail('Email validation error did not appear');
}, 2000);
});

View file

@ -16,6 +16,7 @@ describe('DB API', function () {
before(function (done) {
var app = express();
app.set('disableLoginLimiter', true);
ghost({app: app}).then(function (_httpServer) {
httpServer = _httpServer;

View file

@ -41,7 +41,7 @@ describe('Unauthorized', function () {
});
describe('Unauthorized', function () {
describe('Unauthorized API', function () {
it('can\'t retrieve posts', function (done) {
request.get(testUtils.API.getApiQuery('posts/'))
.expect(401)

View file

@ -16,6 +16,7 @@ describe('Notifications API', function () {
before(function (done) {
var app = express();
app.set('disableLoginLimiter', true);
ghost({app: app}).then(function (_httpServer) {
httpServer = _httpServer;

View file

@ -18,6 +18,7 @@ describe('Post API', function () {
before(function (done) {
var app = express();
app.set('disableLoginLimiter', true);
ghost({app: app}).then(function (_httpServer) {
httpServer = _httpServer;

View file

@ -18,6 +18,7 @@ describe('Settings API', function () {
before(function (done) {
var app = express();
app.set('disableLoginLimiter', true);
ghost({app: app}).then(function (_httpServer) {
httpServer = _httpServer;

View file

@ -18,6 +18,7 @@ describe('Slug API', function () {
before(function (done) {
var app = express();
app.set('disableLoginLimiter', true);
ghost({ app: app }).then(function (_httpServer) {
httpServer = _httpServer;

View file

@ -18,6 +18,7 @@ describe('Tag API', function () {
before(function (done) {
var app = express();
app.set('disableLoginLimiter', true);
ghost({app: app}).then(function (_httpServer) {
httpServer = _httpServer;

View file

@ -16,6 +16,7 @@ describe('User API', function () {
before(function (done) {
var app = express();
app.set('disableLoginLimiter', true);
ghost({app: app}).then(function (_httpServer) {
httpServer = _httpServer;