diff --git a/index.js b/index.js index 3e23e901..3f77c541 100644 --- a/index.js +++ b/index.js @@ -220,6 +220,7 @@ function session(options) { // req.secret is passed from the cookie parser middleware var secrets = secret || [req.secret]; + var originalCookieHash; var originalHash; var originalId; var savedHash; @@ -383,6 +384,7 @@ function session(options) { store.generate(req); originalId = req.sessionID; originalHash = hash(req.session); + originalCookieHash = cookieHash(req.session.cookie); wrapmethods(req.session); } @@ -391,6 +393,7 @@ function session(options) { store.createSession(req, sess) originalId = req.sessionID originalHash = hash(sess) + originalCookieHash = cookieHash(req.session.cookie) if (!resaveSession) { savedHash = originalHash @@ -450,6 +453,11 @@ function session(options) { return originalId === sess.id && savedHash === hash(sess); } + // check if session cookie has been modified + function isCookieModified(sess) { + return originalCookieHash !== cookieHash(sess.cookie); + } + // determine if session should be destroyed function shouldDestroy(req) { return req.sessionID && unsetDestroy && req.session == null; @@ -488,7 +496,7 @@ function session(options) { return cookieId !== req.sessionID ? saveUninitializedSession || isModified(req.session) - : rollingSessions || req.session.cookie.expires != null && isModified(req.session); + : rollingSessions || req.session.cookie.expires != null && isModified(req.session) || isCookieModified(req.session); } // generate a session if the browser doesn't send a sessionID @@ -666,6 +674,35 @@ function hash(sess) { .digest('hex') } +/** + * Hash the given session `cookie` object omitting changes to `.expires`. + * + * The expiry is refreshed every time the session is touched, so it + * cannot take part in detecting changes to the cookie. + * + * @param {Object} cookie + * @return {String} + * @private + */ + +function cookieHash(cookie) { + // serialize + var str = JSON.stringify(cookie.data, function (key, val) { + // ignore expires property + if (key === 'expires') { + return + } + + return val + }) + + // hash + return crypto + .createHash('sha1') + .update(str, 'utf8') + .digest('hex') +} + /** * Determine if request is secure. * diff --git a/test/session.js b/test/session.js index 405e3ddf..a267b174 100644 --- a/test/session.js +++ b/test/session.js @@ -2445,6 +2445,49 @@ describe('session()', function(){ }) }) + describe('.maxAge changed without modifying the session', function () { + before(function (done) { + var ctx = this + + ctx.cookie = '' + ctx.server = createServer({ cookie: { maxAge: 2000 } }, function (req, res) { + if (req.url === '/set') { + req.session.count = 1 + res.end('set') + } else if (req.url === '/extend') { + req.session.cookie.maxAge = 5000 + req.session.touch() + req.session.save(function () { res.end('extended') }) + } else { + res.end('plain') + } + }) + + request(ctx.server) + .get('/set') + .end(function (err, res) { + ctx.cookie = res && cookie(res) + done(err) + }) + }) + + it('should not set-cookie when nothing changed', function (done) { + request(this.server) + .get('/') + .set('Cookie', this.cookie) + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, 'plain', done) + }) + + it('should set-cookie when only the cookie is extended and saved', function (done) { + request(this.server) + .get('/extend') + .set('Cookie', this.cookie) + .expect(shouldSetCookieToExpireIn('connect.sid', 5000)) + .expect(200, 'extended', done) + }) + }) + describe('.expires', function(){ describe('when given a Date', function(){ it('should set absolute', function(done){