Server side cleanup

- remove sessions
- remove all references to csrf
- create a shared base model for the 2 types of token
This commit is contained in:
Hannah Wolfe 2014-07-14 13:29:45 +01:00
parent 4c6b324494
commit 3ff9146d9e
13 changed files with 94 additions and 275 deletions

View File

@ -1,104 +0,0 @@
var Store = require('express-session').Store,
models = require('./models'),
time12h = 12 * 60 * 60 * 1000,
BSStore;
// Initialize store and clean old sessions
BSStore = function BSStore(options) {
var self = this;
options = options || {};
Store.call(this, options);
models.Session.findAll()
.then(function (model) {
var i,
now = new Date().getTime();
for (i = 0; i < model.length; i = i + 1) {
if (now > model.at(i).get('expires')) {
self.destroy(model.at(i).get('id'));
}
}
});
};
BSStore.prototype = new Store();
// store a given session
BSStore.prototype.set = function (sid, sessData, callback) {
var maxAge = sessData.cookie.maxAge,
now = new Date().getTime(),
expires = maxAge ? now + maxAge : now + time12h,
sessionModel = models.Session;
sessData = JSON.stringify(sessData);
//necessary since bookshelf updates models if id is set
sessionModel.forge({id: sid}).fetch()
.then(function (model) {
if (model) {
return sessionModel.forge({id: sid, expires: expires, sess: sessData }).save();
}
return sessionModel.forge({id: sid, expires: expires, sess: sessData })
.save(null, {method: 'insert'});
}).then(function () {
callback();
});
};
// fetch a session, if session is expired delete it
BSStore.prototype.get = function (sid, callback) {
var now = new Date().getTime(),
self = this,
sess,
expires;
models.Session.forge({id: sid})
.fetch()
.then(function (model) {
if (model) {
sess = JSON.parse(model.get('sess'));
expires = model.get('expires');
if (now < expires) {
callback(null, sess);
} else {
self.destroy(sid, callback);
}
} else {
callback();
}
});
};
// delete a given sessions
BSStore.prototype.destroy = function (sid, callback) {
models.Session.forge({id: sid})
.destroy()
.then(function () {
// check if callback is null
// session.regenerate doesn't provide callback
// cleanup at startup does neither
if (callback) {
callback();
}
});
};
// get the count of all stored sessions
BSStore.prototype.length = function (callback) {
models.Session.findAll()
.then(function (model) {
callback(null, model.length);
});
};
// delete all sessions
BSStore.prototype.clear = function (callback) {
models.Session.destroyAll()
.then(function () {
callback();
});
};
module.exports = BSStore;

View File

@ -4,7 +4,7 @@ var _ = require('lodash'),
config = require('../../config'),
utils = require('../utils'),
excludedTables = ['sessions'],
excludedTables = [],
exporter;
exporter = function () {

View File

@ -25,6 +25,8 @@ var api = require('../api'),
authStrategies = require('./authStrategies'),
expressServer,
setupMiddleware,
ONE_HOUR_S = 60 * 60,
ONE_YEAR_S = 365 * 24 * ONE_HOUR_S,
ONE_HOUR_MS = ONE_HOUR_S * 1000,
@ -221,7 +223,7 @@ function robots() {
};
}
module.exports = function (server) {
setupMiddleware = function (server) {
var logging = config().logging,
subdir = config().paths.subdir,
corePath = config().paths.corePath,
@ -319,6 +321,7 @@ module.exports = function (server) {
expressServer.use(errors.error500);
};
module.exports = setupMiddleware;
// Export middleware functions directly
module.exports.middleware = middleware;
// Expose middleware functions in this file as well

View File

@ -3,7 +3,6 @@
// middleware_spec.js
var _ = require('lodash'),
csrf = require('csurf'),
express = require('express'),
busboy = require('./ghost-busboy'),
config = require('../config'),
@ -78,16 +77,6 @@ var middleware = {
next();
},
// Check if we're logged in, and if so, redirect people back to dashboard
// Login and signup forms in particular
redirectToDashboard: function (req, res, next) {
if (req.user && req.user.id) {
return res.redirect(config().paths.subdir + '/ghost/');
}
next();
},
// While we're here, let's clean up on aisle 5
// That being ghost.notifications, and let's remove the passives from there
// plus the local messages, as they have already been added at this point
@ -160,15 +149,6 @@ var middleware = {
});
},
conditionalCSRF: function (req, res, next) {
// CSRF is needed for admin only
if (res.isAdmin) {
csrf()(req, res, next);
return;
}
next();
},
// work around to handle missing client_secret
// oauth2orize needs it, but untrusted clients don't have it
addClientSecret: function (req, res, next) {

View File

@ -1,44 +1,11 @@
var ghostBookshelf = require('./base'),
Basetoken = require('./basetoken'),
Accesstoken,
Accesstokens;
Accesstoken = ghostBookshelf.Model.extend({
tableName: 'accesstokens',
user: function () {
return this.belongsTo('User');
},
client: function () {
return this.belongsTo('Client');
},
// override for base function since we don't have
// a created_by field for sessions
creating: function (newObj, attr, options) {
/*jshint unused:false*/
},
// override for base function since we don't have
// a updated_by field for sessions
saving: function (newObj, attr, options) {
/*jshint unused:false*/
// Remove any properties which don't belong on the model
this.attributes = this.pick(this.permittedAttributes());
}
}, {
destroyAllExpired: function (options) {
options = this.filterOptions(options, 'destroyAll');
return ghostBookshelf.Collection.forge([], {model: this})
.query('where', 'expires', '<', Date.now())
.fetch()
.then(function (collection) {
collection.invokeThen('destroy', options);
});
}
Accesstoken = Basetoken.extend({
tableName: 'accesstokens'
});
Accesstokens = ghostBookshelf.Collection.extend({

View File

@ -1,11 +1,16 @@
var ghostBookshelf = require('./base'),
Session,
Sessions;
Basetoken;
Session = ghostBookshelf.Model.extend({
Basetoken = ghostBookshelf.Model.extend({
tableName: 'sessions',
user: function () {
return this.belongsTo('User');
},
client: function () {
return this.belongsTo('Client');
},
// override for base function since we don't have
// a created_by field for sessions
@ -22,20 +27,15 @@ Session = ghostBookshelf.Model.extend({
}
}, {
destroyAll: function (options) {
destroyAllExpired: function (options) {
options = this.filterOptions(options, 'destroyAll');
return ghostBookshelf.Collection.forge([], {model: this}).fetch()
return ghostBookshelf.Collection.forge([], {model: this})
.query('where', 'expires', '<', Date.now())
.fetch()
.then(function (collection) {
collection.invokeThen('destroy', options);
});
}
});
Sessions = ghostBookshelf.Collection.extend({
model: Session
});
module.exports = {
Session: ghostBookshelf.model('Session', Session),
Sessions: ghostBookshelf.collection('Sessions', Sessions)
};
module.exports = Basetoken;

View File

@ -11,7 +11,6 @@ models = {
Settings: require('./settings').Settings,
Tag: require('./tag').Tag,
Base: require('./base'),
Session: require('./session').Session,
App: require('./app').App,
AppField: require('./appField').AppField,
AppSetting: require('./appSetting').AppSetting,

View File

@ -1,44 +1,11 @@
var ghostBookshelf = require('./base'),
Basetoken = require('./basetoken'),
Refreshtoken,
Refreshtokens;
Refreshtoken = ghostBookshelf.Model.extend({
tableName: 'refreshtokens',
user: function () {
return this.belongsTo('User');
},
client: function () {
return this.belongsTo('Client');
},
// override for base function since we don't have
// a created_by field for sessions
creating: function (newObj, attr, options) {
/*jshint unused:false*/
},
// override for base function since we don't have
// a updated_by field for sessions
saving: function (newObj, attr, options) {
/*jshint unused:false*/
// Remove any properties which don't belong on the model
this.attributes = this.pick(this.permittedAttributes());
}
}, {
destroyAllExpired: function (options) {
options = this.filterOptions(options, 'destroyAll');
return ghostBookshelf.Collection.forge([], {model: this})
.query('where', 'expires', '<', Date.now())
.fetch()
.then(function (collection) {
collection.invokeThen('destroy', options);
});
}
Refreshtoken = Basetoken.extend({
tableName: 'refreshtokens'
});
Refreshtokens = ghostBookshelf.Collection.extend({

View File

@ -12,22 +12,12 @@ adminRoutes = function (middleware) {
subdir = config().paths.subdir;
// ### Admin routes
router.get('^/logout/', function redirect(req, res) {
router.get(/^\/(logout|signout)\/$/, function redirect(req, res) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/ghost/signout/');
});
router.get('^/signout/', function redirect(req, res) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/ghost/signout/');
});
router.get('^/signin/', function redirect(req, res) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/ghost/signin/');
});
router.get('^/signup/', function redirect(req, res) {
router.get(/^\/signup\/$/, function redirect(req, res) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/ghost/signup/');
@ -36,7 +26,7 @@ adminRoutes = function (middleware) {
router.post('/ghost/upload/', middleware.busboy, admin.upload);
// redirect to /ghost and let that do the authentication to prevent redirects to /ghost//admin etc.
router.get(/^\/((ghost-admin|admin|wp-admin|dashboard|signin)\/?)$/, function (req, res) {
router.get(/^\/((ghost-admin|admin|wp-admin|dashboard|signin|login)\/?)$/, function (req, res) {
/*jslint unparam:true*/
res.redirect(subdir + '/ghost/');
});

View File

@ -1,3 +1,6 @@
var utils,
getRandomInt;
/**
* Return a random int, used by `utils.uid()`
*
@ -6,11 +9,11 @@
* @return {Number}
* @api private
*/
function getRandomInt(min, max) {
getRandomInt = function (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
};
var utils = {
utils = {
/**
* Return a unique identifier with the given `len`.
*

View File

@ -4,7 +4,6 @@
<head>
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="csrf-param" content="{{csrfToken}}" />
<title>Ghost Admin</title>

View File

@ -92,14 +92,6 @@ describe('Admin Routing', function () {
.end(doEndNoAuth(done));
});
it('should redirect /signin/ to /ghost/signin/', function (done) {
request.get('/signin/')
.expect('Location', '/ghost/signin/')
.expect('Cache-Control', cacheRules.year)
.expect(301)
.end(doEndNoAuth(done));
});
it('should redirect /signup/ to /ghost/signup/', function (done) {
request.get('/signup/')
.expect('Location', '/ghost/signup/')
@ -107,6 +99,24 @@ describe('Admin Routing', function () {
.expect(301)
.end(doEndNoAuth(done));
});
// Admin aliases
it('should redirect /signin/ to /ghost/', function (done) {
request.get('/signin/')
.expect('Location', '/ghost/')
.expect('Cache-Control', cacheRules.public)
.expect(302)
.end(doEndNoAuth(done));
});
it('should redirect /admin/ to /ghost/', function (done) {
request.get('/admin/')
.expect('Location', '/ghost/')
.expect('Cache-Control', cacheRules.public)
.expect(302)
.end(doEndNoAuth(done));
});
// there are more of these... but we get the point
});
// we'll use X-Forwarded-Proto: https to simulate an 'https://' request behind a proxy
@ -184,7 +194,6 @@ describe('Admin Routing', function () {
});
describe('Ghost Admin Setup', function () {
it('should redirect from /ghost/ to /ghost/setup/ when no user/not installed yet', function (done) {
request.get('/ghost/')
.expect('Location', /ghost\/setup/)
@ -230,9 +239,22 @@ describe('Admin Routing', function () {
// });
});
// TODO: new test for Ember where user is added
//
// describe('Ghost Admin Forgot Password', function () {
// before(function (done) {
// // Create a user / do setup etc
// testUtils.clearData()
// .then(function () {
// return testUtils.initData();
// })
// .then(function () {
// return testUtils.insertDefaultFixtures();
// }).then(function () {
// done();
// })
// .catch(done);
// });
//
// it('should respond with html for /ghost/forgotten/', function (done) {
// request.get('/ghost/forgotten/')
// .expect('Content-Type', /html/)
@ -240,7 +262,7 @@ describe('Admin Routing', function () {
// .expect(200)
// .end(doEnd(done));
// });
//
// it('should respond 404 for /ghost/reset/', function (done) {
// request.get('/ghost/reset/')
// .expect('Cache-Control', cacheRules['private'])
@ -248,7 +270,7 @@ describe('Admin Routing', function () {
// .expect(/Page Not Found/)
// .end(doEnd(done));
// });
//
// it('should redirect /ghost/reset/*/', function (done) {
// request.get('/ghost/reset/athing/')
// .expect('Location', /ghost\/forgotten/)
@ -261,8 +283,7 @@ describe('Admin Routing', function () {
// TODO: not working anymore, needs new test for Ember
// describe('Authenticated Admin Routing', function () {
// var user = testUtils.DataGenerator.forModel.users[0],
// csrfToken = '';
// var user = testUtils.DataGenerator.forModel.users[0];
// before(function (done) {
// var app = express();
@ -287,13 +308,9 @@ describe('Admin Routing', function () {
// return done(err);
// }
// var pattern_meta = /<meta.*?name="csrf-param".*?content="(.*?)".*?>/i;
// pattern_meta.should.exist;
// csrfToken = res.text.match(pattern_meta)[1];
// process.nextTick(function() {
// request.post('/ghost/signin/')
// .set('X-CSRF-Token', csrfToken)
// .send({email: user.email, password: user.password})
// .expect(200)
// .end(function (err, res) {
@ -309,7 +326,6 @@ describe('Admin Routing', function () {
// return done(err);
// }
// csrfToken = res.text.match(pattern_meta)[1];
// done();
// });
// });

View File

@ -40,7 +40,6 @@
"cookie-parser": "1.0.1",
"connect": "3.0.0-rc.1",
"connect-slashes": "1.2.0",
"csurf": "1.1.0",
"downsize": "0.0.5",
"express": "4.1.1",
"express-hbs": "0.7.10",