Merge pull request #2249 from overleaf/bg-create-session-validation-token-for-all-sessions

move session validation into SessionStoreManager

GitOrigin-RevId: 438ec738f0ccaf9f072629d6203a334cb8439b83
This commit is contained in:
Brian Gough
2019-10-18 08:42:35 +00:00
committed by sharelatex
parent 5d3c125ffb
commit bc8ccf26c7
5 changed files with 141 additions and 80 deletions
@@ -8,6 +8,7 @@ const Settings = require('settings-sharelatex')
const basicAuth = require('basic-auth-connect')
const UserHandler = require('../User/UserHandler')
const UserSessionsManager = require('../User/UserSessionsManager')
const SessionStoreManager = require('../../infrastructure/SessionStoreManager')
const Analytics = require('../Analytics/AnalyticsManager')
const passport = require('passport')
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
@@ -42,51 +43,6 @@ const AuthenticationController = (module.exports = {
cb(null, user)
},
computeSessionValidationToken(req) {
// this should be a deterministic function of the client-side sessionID,
// prepended with a version number in case we want to change it later
return 'v1:' + req.sessionID.slice(-4)
},
setSessionValidationToken(req) {
req.session.validationToken = AuthenticationController.computeSessionValidationToken(
req
)
},
checkSessionValidationToken(req) {
if (req.session) {
const token = req.session.validationToken
if (token) {
const clientToken = AuthenticationController.computeSessionValidationToken(
req
)
// Reject invalid sessions. If you change the method for computing the
// token (above) then you need to either check or ignore previous
// versions of the token.
if (token === clientToken) {
Metrics.inc('security.session', 1, { status: 'ok' })
return true
} else {
logger.error(
{
token: token,
clientToken: clientToken,
req: req,
sessionID: req.sessionID
},
'session token validation failed'
)
Metrics.inc('security.session', 1, { status: 'error' })
return false
}
} else {
Metrics.inc('security.session', 1, { status: 'missing' })
}
}
return true // fallback to allowing session
},
afterLoginSessionSetup(req, user, callback) {
if (callback == null) {
callback = function() {}
@@ -108,14 +64,14 @@ const AuthenticationController = (module.exports = {
return callback(err)
}
req.sessionStore.generate(req)
// Note: the validation token is not writable, so it does not get
// transferred to the new session below.
for (let key in oldSession) {
const value = oldSession[key]
if (key !== '__tmp') {
req.session[key] = value
}
}
// generate an identifier from the sessionID
AuthenticationController.setSessionValidationToken(req)
req.session.save(function(err) {
if (err) {
logger.warn(
@@ -299,7 +255,7 @@ const AuthenticationController = (module.exports = {
},
getSessionUser(req) {
if (!AuthenticationController.checkSessionValidationToken(req)) {
if (!SessionStoreManager.checkValidationToken(req)) {
return null
}
const sessionUser = _.get(req, ['session', 'user'])
@@ -13,6 +13,7 @@ const Csrf = require('./Csrf')
const sessionsRedisClient = UserSessionsRedis.client()
const SessionStoreManager = require('./SessionStoreManager')
const session = require('express-session')
const RedisStore = require('connect-redis')(session)
const bodyParser = require('body-parser')
@@ -111,6 +112,9 @@ webRouter.use(
})
)
// patch the session store to generate a validation token for every new session
SessionStoreManager.enableValidationToken(sessionStore)
// passport
webRouter.use(passport.initialize())
webRouter.use(passport.session())
@@ -0,0 +1,54 @@
const Metrics = require('metrics-sharelatex')
const logger = require('logger-sharelatex')
function computeValidationToken(req) {
// this should be a deterministic function of the client-side sessionID,
// prepended with a version number in case we want to change it later
return 'v1:' + req.sessionID.slice(-4)
}
module.exports = {
enableValidationToken(sessionStore) {
// generate an identifier from the sessionID for every new session
const originalGenerate = sessionStore.generate
sessionStore.generate = function(req) {
originalGenerate(req)
// add the validation token as a property that cannot be overwritten
Object.defineProperty(req.session, 'validationToken', {
value: computeValidationToken(req),
enumerable: true,
writable: false
})
Metrics.inc('security.session', 1, { status: 'new' })
}
},
checkValidationToken(req) {
if (req.session) {
const sessionToken = req.session.validationToken
if (sessionToken) {
const clientToken = computeValidationToken(req)
// Reject invalid sessions. If you change the method for computing the
// token (above) then you need to either check or ignore previous
// versions of the token.
if (sessionToken === clientToken) {
Metrics.inc('security.session', 1, { status: 'ok' })
return true
} else {
logger.error(
{
sessionToken: sessionToken,
clientToken: clientToken
},
'session token validation failed'
)
Metrics.inc('security.session', 1, { status: 'error' })
return false
}
} else {
Metrics.inc('security.session', 1, { status: 'missing' })
}
}
return true // fallback to allowing session
}
}