mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
Refactored session service (#11701)
* Refactored SessionStore to use @tryghost/errors no-issue * Updated tests to test exposed API no-issue This will make refactoring easier, as we only have the "public" contract to maintain * Refactored session functionality to SessionService no-issue This splits the session logic away from the HTTP responding logic, which will allows us to decouple session creation/modification from the API. Eventually this can be used to create sessions based on magiclink style tokens. * Instantiated and exported the new SessionService no-issue * Refactored session middleware to take session service no-issue This removes duplication of code and makes the middleware more explicit that it's just a wrapper around the session service. * Updated to use external @tryghost/session-service no-issue
This commit is contained in:
parent
5a748ee5a9
commit
23154f0739
9 changed files with 169 additions and 184 deletions
|
@ -1,14 +1,123 @@
|
||||||
|
const session = require('express-session');
|
||||||
|
const constants = require('../../../lib/constants');
|
||||||
|
const config = require('../../../config');
|
||||||
|
const settingsCache = require('../../settings/cache');
|
||||||
|
const models = require('../../../models');
|
||||||
|
const urlUtils = require('../../../lib/url-utils');
|
||||||
|
const url = require('url');
|
||||||
|
|
||||||
|
const SessionService = require('@tryghost/session-service');
|
||||||
|
const SessionMiddleware = require('./middleware');
|
||||||
|
const SessionStore = require('./store');
|
||||||
|
|
||||||
|
function getOriginOfRequest(req) {
|
||||||
|
const origin = req.get('origin');
|
||||||
|
const referrer = req.get('referrer');
|
||||||
|
|
||||||
|
if (!origin && !referrer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (origin) {
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {protocol, host} = url.parse(referrer);
|
||||||
|
if (protocol && host) {
|
||||||
|
return `${protocol}//${host}`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSession(req, res) {
|
||||||
|
if (req.session) {
|
||||||
|
return req.session;
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
expressSessionMiddleware(req, res, function (err) {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
resolve(req.session);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findUserById({id}) {
|
||||||
|
return models.User.findOne({id});
|
||||||
|
}
|
||||||
|
|
||||||
|
let expressSessionMiddleware;
|
||||||
|
function initExpressSessionMiddleware() {
|
||||||
|
if (!expressSessionMiddleware) {
|
||||||
|
expressSessionMiddleware = session({
|
||||||
|
store: new SessionStore(models.Session),
|
||||||
|
secret: settingsCache.get('session_secret'),
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
name: 'ghost-admin-api-session',
|
||||||
|
cookie: {
|
||||||
|
maxAge: constants.SIX_MONTH_MS,
|
||||||
|
httpOnly: true,
|
||||||
|
path: urlUtils.getSubdir() + '/ghost',
|
||||||
|
sameSite: 'lax',
|
||||||
|
secure: urlUtils.isSSL(config.get('url'))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sessionService;
|
||||||
|
function initSessionService() {
|
||||||
|
if (!sessionService) {
|
||||||
|
if (!expressSessionMiddleware) {
|
||||||
|
initExpressSessionMiddleware();
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionService = SessionService({
|
||||||
|
getOriginOfRequest,
|
||||||
|
getSession,
|
||||||
|
findUserById
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sessionMiddleware;
|
||||||
|
function initSessionMiddleware() {
|
||||||
|
if (!sessionMiddleware) {
|
||||||
|
if (!sessionService) {
|
||||||
|
initSessionService();
|
||||||
|
}
|
||||||
|
sessionMiddleware = SessionMiddleware({
|
||||||
|
sessionService
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// @TODO: expose files/units and not functions of units
|
|
||||||
get createSession() {
|
get createSession() {
|
||||||
return require('./middleware').createSession;
|
return this.middleware.createSession;
|
||||||
},
|
},
|
||||||
|
|
||||||
get destroySession() {
|
get destroySession() {
|
||||||
return require('./middleware').destroySession;
|
return this.middleware.destroySession;
|
||||||
},
|
},
|
||||||
|
|
||||||
get authenticate() {
|
get authenticate() {
|
||||||
return require('./middleware').authenticate;
|
return this.middleware.authenticate;
|
||||||
|
},
|
||||||
|
|
||||||
|
get service() {
|
||||||
|
if (!sessionService) {
|
||||||
|
initSessionService();
|
||||||
|
}
|
||||||
|
return sessionService;
|
||||||
|
},
|
||||||
|
|
||||||
|
get middleware() {
|
||||||
|
if (!sessionMiddleware) {
|
||||||
|
initSessionMiddleware();
|
||||||
|
}
|
||||||
|
return sessionMiddleware;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,140 +1,37 @@
|
||||||
const url = require('url');
|
function SessionMiddleware({sessionService}) {
|
||||||
const session = require('express-session');
|
async function createSession(req, res, next) {
|
||||||
const common = require('../../../lib/common');
|
|
||||||
const constants = require('../../../lib/constants');
|
|
||||||
const config = require('../../../config');
|
|
||||||
const settingsCache = require('../../settings/cache');
|
|
||||||
const models = require('../../../models');
|
|
||||||
const SessionStore = require('./store');
|
|
||||||
const urlUtils = require('../../../lib/url-utils');
|
|
||||||
|
|
||||||
const getOrigin = (req) => {
|
|
||||||
const origin = req.get('origin');
|
|
||||||
const referrer = req.get('referrer');
|
|
||||||
|
|
||||||
if (!origin && !referrer) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (origin) {
|
|
||||||
return origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {protocol, host} = url.parse(referrer);
|
|
||||||
if (protocol && host) {
|
|
||||||
return `${protocol}//${host}`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
let UNO_SESSIONIONA;
|
|
||||||
const getSession = (req, res, next) => {
|
|
||||||
if (!UNO_SESSIONIONA) {
|
|
||||||
UNO_SESSIONIONA = session({
|
|
||||||
store: new SessionStore(models.Session),
|
|
||||||
secret: settingsCache.get('session_secret'),
|
|
||||||
resave: false,
|
|
||||||
saveUninitialized: false,
|
|
||||||
name: 'ghost-admin-api-session',
|
|
||||||
cookie: {
|
|
||||||
maxAge: constants.SIX_MONTH_MS,
|
|
||||||
httpOnly: true,
|
|
||||||
path: urlUtils.getSubdir() + '/ghost',
|
|
||||||
sameSite: 'lax',
|
|
||||||
secure: urlUtils.isSSL(config.get('url'))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return UNO_SESSIONIONA(req, res, next);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createSession = (req, res, next) => {
|
|
||||||
getSession(req, res, function (err) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
const origin = getOrigin(req);
|
|
||||||
if (!origin) {
|
|
||||||
return next(new common.errors.BadRequestError({
|
|
||||||
message: common.i18n.t('errors.middleware.auth.unknownOrigin')
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
req.session.user_id = req.user.id;
|
|
||||||
req.session.origin = origin;
|
|
||||||
req.session.user_agent = req.get('user-agent');
|
|
||||||
req.session.ip = req.ip;
|
|
||||||
res.sendStatus(201);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const destroySession = (req, res, next) => {
|
|
||||||
req.session.destroy((err) => {
|
|
||||||
if (err) {
|
|
||||||
return next(new common.errors.InternalServerError({err}));
|
|
||||||
}
|
|
||||||
return res.sendStatus(204);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cookieCsrfProtection = (req) => {
|
|
||||||
// If there is no origin on the session object it means this is a *new*
|
|
||||||
// session, that hasn't been initialised yet. So we don't need CSRF protection
|
|
||||||
if (!req.session.origin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const origin = getOrigin(req);
|
|
||||||
|
|
||||||
if (req.session.origin !== origin) {
|
|
||||||
throw new common.errors.BadRequestError({
|
|
||||||
message: common.i18n.t('errors.middleware.auth.mismatchedOrigin', {
|
|
||||||
expected: req.session.origin,
|
|
||||||
actual: origin
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const authenticate = (req, res, next) => {
|
|
||||||
// CASE: we don't have a cookie header so allow fallthrough to other
|
|
||||||
// auth middleware or final "ensure authenticated" check
|
|
||||||
if (!req.headers || !req.headers.cookie) {
|
|
||||||
req.user = null;
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
getSession(req, res, function (err) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cookieCsrfProtection(req);
|
await sessionService.createSessionForUser(req, res, req.user);
|
||||||
|
res.sendStatus(201);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return next(err);
|
next(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.session || !req.session.user_id) {
|
async function destroySession(req, res, next) {
|
||||||
req.user = null;
|
try {
|
||||||
return next();
|
await sessionService.destroyCurrentSession(req);
|
||||||
|
res.sendStatus(204);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
models.User.findOne({id: req.session.user_id})
|
async function authenticate(req, res, next) {
|
||||||
.then((user) => {
|
try {
|
||||||
|
const user = await sessionService.getUserForSession(req, res);
|
||||||
req.user = user;
|
req.user = user;
|
||||||
next();
|
next();
|
||||||
})
|
} catch (err) {
|
||||||
.catch(() => {
|
next(err);
|
||||||
req.user = null;
|
}
|
||||||
next();
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// @TODO: this interface exposes private functions
|
return {
|
||||||
module.exports = exports = {
|
createSession: createSession,
|
||||||
createSession,
|
destroySession: destroySession,
|
||||||
destroySession,
|
authenticate: authenticate
|
||||||
cookieCsrfProtection,
|
};
|
||||||
authenticate
|
}
|
||||||
};
|
|
||||||
|
module.exports = SessionMiddleware;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const {Store} = require('express-session');
|
const {Store} = require('express-session');
|
||||||
const common = require('../../../lib/common');
|
const {InternalServerError} = require('@tryghost/errors');
|
||||||
|
|
||||||
module.exports = class SessionStore extends Store {
|
module.exports = class SessionStore extends Store {
|
||||||
constructor(SessionModel) {
|
constructor(SessionModel) {
|
||||||
|
@ -30,8 +30,8 @@ module.exports = class SessionStore extends Store {
|
||||||
|
|
||||||
set(sid, sessionData, callback) {
|
set(sid, sessionData, callback) {
|
||||||
if (!sessionData.user_id) {
|
if (!sessionData.user_id) {
|
||||||
return callback(new common.errors.InternalServerError({
|
return callback(new InternalServerError({
|
||||||
message: common.i18n.t('errors.middleware.auth.missingUserID')
|
message: 'Cannot create a session with no user_id'
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
this.SessionModel
|
this.SessionModel
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
"@tryghost/kg-markdown-html-renderer": "1.0.2",
|
"@tryghost/kg-markdown-html-renderer": "1.0.2",
|
||||||
"@tryghost/members-api": "0.18.0",
|
"@tryghost/members-api": "0.18.0",
|
||||||
"@tryghost/members-ssr": "0.7.4",
|
"@tryghost/members-ssr": "0.7.4",
|
||||||
|
"@tryghost/session-service": "^0.1.0",
|
||||||
"@tryghost/social-urls": "0.1.7",
|
"@tryghost/social-urls": "0.1.7",
|
||||||
"@tryghost/string": "0.1.7",
|
"@tryghost/string": "0.1.7",
|
||||||
"@tryghost/url-utils": "0.6.16",
|
"@tryghost/url-utils": "0.6.16",
|
||||||
|
|
|
@ -5,7 +5,7 @@ const models = require('../../../../core/server/models');
|
||||||
const {UnauthorizedError} = require('../../../../core/server/lib/common/errors');
|
const {UnauthorizedError} = require('../../../../core/server/lib/common/errors');
|
||||||
|
|
||||||
const sessionController = require('../../../../core/server/api/canary/session');
|
const sessionController = require('../../../../core/server/api/canary/session');
|
||||||
const sessionServiceMiddleware = require('../../../../core/server/services/auth/session/middleware');
|
const sessionServiceMiddleware = require('../../../../core/server/services/auth/session').middleware;
|
||||||
|
|
||||||
describe('Session controller', function () {
|
describe('Session controller', function () {
|
||||||
before(function () {
|
before(function () {
|
||||||
|
|
|
@ -5,7 +5,7 @@ const models = require('../../../../core/server/models');
|
||||||
const {UnauthorizedError} = require('../../../../core/server/lib/common/errors');
|
const {UnauthorizedError} = require('../../../../core/server/lib/common/errors');
|
||||||
|
|
||||||
const sessionController = require('../../../../core/server/api/v2/session');
|
const sessionController = require('../../../../core/server/api/v2/session');
|
||||||
const sessionServiceMiddleware = require('../../../../core/server/services/auth/session/middleware');
|
const sessionServiceMiddleware = require('../../../../core/server/services/auth/session').middleware;
|
||||||
|
|
||||||
describe('v2 Session controller', function () {
|
describe('v2 Session controller', function () {
|
||||||
before(function () {
|
before(function () {
|
||||||
|
|
|
@ -5,7 +5,7 @@ const models = require('../../../../core/server/models');
|
||||||
const {UnauthorizedError} = require('../../../../core/server/lib/common/errors');
|
const {UnauthorizedError} = require('../../../../core/server/lib/common/errors');
|
||||||
|
|
||||||
const sessionController = require('../../../../core/server/api/canary/session');
|
const sessionController = require('../../../../core/server/api/canary/session');
|
||||||
const sessionServiceMiddleware = require('../../../../core/server/services/auth/session/middleware');
|
const sessionServiceMiddleware = require('../../../../core/server/services/auth/session').middleware;
|
||||||
|
|
||||||
describe('v3 Session controller', function () {
|
describe('v3 Session controller', function () {
|
||||||
before(function () {
|
before(function () {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const sessionMiddleware = require('../../../../../core/server/services/auth/session/middleware');
|
const sessionMiddleware = require('../../../../../core/server/services/auth').session;
|
||||||
const models = require('../../../../../core/server/models');
|
const models = require('../../../../../core/server/models');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
const should = require('should');
|
const should = require('should');
|
||||||
|
@ -93,14 +93,21 @@ describe('Session Service', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('destroySession', function () {
|
describe('destroySession', function () {
|
||||||
it('calls req.session.destroy', function () {
|
it('calls req.session.destroy', function (done) {
|
||||||
const req = fakeReq();
|
const req = fakeReq();
|
||||||
const res = fakeRes();
|
const res = fakeRes();
|
||||||
const destroyStub = sinon.stub(req.session, 'destroy');
|
const destroyStub = sinon.stub(req.session, 'destroy')
|
||||||
|
.callsFake(function (fn) {
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
sinon.stub(res, 'sendStatus')
|
||||||
|
.callsFake(function (statusCode) {
|
||||||
|
should.equal(destroyStub.callCount, 1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
sessionMiddleware.destroySession(req, res);
|
sessionMiddleware.destroySession(req, res);
|
||||||
|
|
||||||
should.equal(destroyStub.callCount, 1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls next with InternalServerError if destroy errors', function (done) {
|
it('calls next with InternalServerError if destroy errors', function (done) {
|
||||||
|
@ -133,40 +140,4 @@ describe('Session Service', function () {
|
||||||
sessionMiddleware.destroySession(req, res);
|
sessionMiddleware.destroySession(req, res);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CSRF protection', function () {
|
|
||||||
it('calls next if the session is uninitialized', function (done) {
|
|
||||||
const req = fakeReq();
|
|
||||||
const res = fakeRes();
|
|
||||||
|
|
||||||
sessionMiddleware.cookieCsrfProtection(req);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls next if req origin matches the session origin', function (done) {
|
|
||||||
const req = fakeReq();
|
|
||||||
const res = fakeRes();
|
|
||||||
sinon.stub(req, 'get')
|
|
||||||
.withArgs('origin').returns('http://host.tld');
|
|
||||||
req.session.origin = 'http://host.tld';
|
|
||||||
|
|
||||||
sessionMiddleware.cookieCsrfProtection(req);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls next with BadRequestError if the origin of req does not match the session', function (done) {
|
|
||||||
const req = fakeReq();
|
|
||||||
const res = fakeRes();
|
|
||||||
sinon.stub(req, 'get')
|
|
||||||
.withArgs('origin').returns('http://host.tld');
|
|
||||||
req.session.origin = 'http://different-host.tld';
|
|
||||||
|
|
||||||
try {
|
|
||||||
sessionMiddleware.cookieCsrfProtection(req);
|
|
||||||
} catch (err) {
|
|
||||||
should.equal(err instanceof BadRequestError, true);
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -448,6 +448,13 @@
|
||||||
request "^2.88.0"
|
request "^2.88.0"
|
||||||
request-promise "^4.2.4"
|
request-promise "^4.2.4"
|
||||||
|
|
||||||
|
"@tryghost/session-service@^0.1.0":
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tryghost/session-service/-/session-service-0.1.0.tgz#3e37f1047b6404cbac59de0953ef1246657e742d"
|
||||||
|
integrity sha512-yAnJ0Bnl5FReA8mrL+J7nc7S9QWobegaOqK0lTHQpbCHdlkXaDdkNYFniztE8BClWmHrRrtRfHLTN+eovEpenA==
|
||||||
|
dependencies:
|
||||||
|
"@tryghost/errors" "^0.1.1"
|
||||||
|
|
||||||
"@tryghost/social-urls@0.1.7":
|
"@tryghost/social-urls@0.1.7":
|
||||||
version "0.1.7"
|
version "0.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/@tryghost/social-urls/-/social-urls-0.1.7.tgz#a62b008c16e2e1f6d7519a9f36f3b2966be2bad8"
|
resolved "https://registry.yarnpkg.com/@tryghost/social-urls/-/social-urls-0.1.7.tgz#a62b008c16e2e1f6d7519a9f36f3b2966be2bad8"
|
||||||
|
|
Loading…
Add table
Reference in a new issue