diff --git a/lib/http/request.js b/lib/http/request.js index 01985393..cdb5432f 100644 --- a/lib/http/request.js +++ b/lib/http/request.js @@ -36,7 +36,7 @@ req.logIn = function(user, options, done) { if (typeof done != 'function') { throw new Error('req#login requires a callback function'); } var self = this; - this._sessionManager.logIn(this, user, function(err) { + this._sessionManager.logIn(this, user, options, function(err) { if (err) { self[property] = null; return done(err); } done(); }); @@ -51,12 +51,22 @@ req.logIn = function(user, options, done) { * @api public */ req.logout = -req.logOut = function() { +req.logOut = function(options, done) { + if (typeof options == 'function') { + done = options; + options = {}; + } + options = options || {}; + var property = this._userProperty || 'user'; this[property] = null; if (this._sessionManager) { - this._sessionManager.logOut(this); + if (typeof done != 'function') { throw new Error('req#logout requires a callback function'); } + + this._sessionManager.logOut(this, options, done); + } else { + done && done(); } }; diff --git a/lib/sessionmanager.js b/lib/sessionmanager.js index 3d5c51ca..81b59b1d 100644 --- a/lib/sessionmanager.js +++ b/lib/sessionmanager.js @@ -1,3 +1,5 @@ +var merge = require('utils-merge'); + function SessionManager(options, serializeUser) { if (typeof options == 'function') { serializeUser = options; @@ -9,30 +11,85 @@ function SessionManager(options, serializeUser) { this._serializeUser = serializeUser; } -SessionManager.prototype.logIn = function(req, user, cb) { +SessionManager.prototype.logIn = function(req, user, options, cb) { + if (typeof options == 'function') { + cb = options; + options = {}; + } + options = options || {}; + + if (!req.session) { return cb(new Error('Login sessions require session support. Did you forget to use `express-session` middleware?')); } + var self = this; - this._serializeUser(user, req, function(err, obj) { + var prevSession = req.session; + + // regenerate the session, which is good practice to help + // guard against forms of session fixation + req.session.regenerate(function(err) { if (err) { return cb(err); } - // TODO: Error if session isn't available here. - if (!req.session) { - req.session = {}; - } - if (!req.session[self._key]) { - req.session[self._key] = {}; - } - req.session[self._key].user = obj; - cb(); + + self._serializeUser(user, req, function(err, obj) { + if (err) { + return cb(err); + } + if (options.keepSessionInfo) { + merge(req.session, prevSession); + } + if (!req.session[self._key]) { + req.session[self._key] = {}; + } + // store user information in session, typically a user id + req.session[self._key].user = obj; + // save the session before redirection to ensure page + // load does not happen before session is saved + req.session.save(function(err) { + if (err) { + return cb(err); + } + cb(); + }); + }); }); } -SessionManager.prototype.logOut = function(req, cb) { - if (req.session && req.session[this._key]) { +SessionManager.prototype.logOut = function(req, options, cb) { + if (typeof options == 'function') { + cb = options; + options = {}; + } + options = options || {}; + + if (!req.session) { return cb(new Error('Login sessions require session support. Did you forget to use `express-session` middleware?')); } + + var self = this; + + // clear the user from the session object and save. + // this will ensure that re-using the old session id + // does not have a logged in user + if (req.session[this._key]) { delete req.session[this._key].user; } + var prevSession = req.session; - cb && cb(); + req.session.save(function(err) { + if (err) { + return cb(err) + } + + // regenerate the session, which is good practice to help + // guard against forms of session fixation + req.session.regenerate(function(err) { + if (err) { + return cb(err); + } + if (options.keepSessionInfo) { + merge(req.session, prevSession); + } + cb(); + }); + }); } diff --git a/package.json b/package.json index 53cbd220..00398a85 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "main": "./lib", "dependencies": { "passport-strategy": "1.x.x", - "pause": "0.0.1" + "pause": "0.0.1", + "utils-merge": "^1.0.1" }, "devDependencies": { "make-node": "0.3.x", diff --git a/test/http/request.test.js b/test/http/request.test.js index 29137fd2..d94dc9f2 100644 --- a/test/http/request.test.js +++ b/test/http/request.test.js @@ -202,7 +202,72 @@ describe('http.ServerRequest', function() { req._passport = {}; req._passport.instance = passport; req._sessionManager = passport._sm; - req.session = {}; + req.session = { id: '1' }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + req.session.save = function(cb) { + process.nextTick(cb); + }; + process.nextTick(cb); + }; + + var error; + + before(function(done) { + var user = { id: '1', username: 'root' }; + + req.login(user, function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); + + it('should be authenticated', function() { + expect(req.isAuthenticated()).to.be.true; + expect(req.isUnauthenticated()).to.be.false; + }); + + it('should regenerate session', function() { + expect(req.session.id).to.equal('2'); + }); + + it('should set user', function() { + expect(req.user).to.be.an('object'); + expect(req.user.id).to.equal('1'); + expect(req.user.username).to.equal('root'); + }); + + it('should serialize user', function() { + expect(req.session['passport'].user).to.equal('1'); + }); + }); + + describe('establishing a session and not keeping previous session data', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { cart: [ '1', '2', ] }; + Object.defineProperty(req.session, 'id', { value: '1' }); + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + req.session.save = function(cb) { + process.nextTick(cb); + }; + process.nextTick(cb); + }; var error; @@ -224,6 +289,76 @@ describe('http.ServerRequest', function() { expect(req.isUnauthenticated()).to.be.false; }); + it('should regenerate session', function() { + expect(req.session.id).to.equal('2'); + }); + + it('should not keep session data', function() { + expect(req.session.cart).to.be.undefined; + }); + + it('should set user', function() { + expect(req.user).to.be.an('object'); + expect(req.user.id).to.equal('1'); + expect(req.user.username).to.equal('root'); + }); + + it('should serialize user', function() { + expect(req.session['passport'].user).to.equal('1'); + }); + }); + + describe('establishing a session and keeping previous session data', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { cart: [ '1', '2', ] }; + Object.defineProperty(req.session, 'id', { value: '1' }); + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + req.session.save = function(cb) { + process.nextTick(cb); + }; + process.nextTick(cb); + }; + + var error; + + before(function(done) { + var user = { id: '1', username: 'root' }; + + req.login(user, { keepSessionInfo: true }, function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); + + it('should be authenticated', function() { + expect(req.isAuthenticated()).to.be.true; + expect(req.isUnauthenticated()).to.be.false; + }); + + it('should regenerate session', function() { + expect(req.session.id).to.equal('2'); + }); + + it('should keep session data', function() { + expect(req.session.cart).to.deep.equal([ '1', '2' ]); + }); + it('should set user', function() { expect(req.user).to.be.an('object'); expect(req.user.id).to.equal('1'); @@ -248,7 +383,14 @@ describe('http.ServerRequest', function() { req._passport = {}; req._passport.instance = passport; req._sessionManager = passport._sm; - req.session = {}; + req.session = { id: '1' }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + req.session.save = function(cb) { + process.nextTick(cb); + }; + process.nextTick(cb); + } req._userProperty = 'currentUser'; var error; @@ -271,6 +413,10 @@ describe('http.ServerRequest', function() { expect(req.isUnauthenticated()).to.be.false; }); + it('should regenerate session', function() { + expect(req.session.id).to.equal('2'); + }); + it('should not set user', function() { expect(req.user).to.be.undefined; }); @@ -286,6 +432,61 @@ describe('http.ServerRequest', function() { }); }); + describe('encountering an error when regenerating session', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session['passport'] = {}; + req.session.regenerate = function(cb) { + process.nextTick(function(){ + cb(new Error('something went wrong')); + }) + } + + var error; + + before(function(done) { + var user = { id: '1', username: 'root' }; + + req.login(user, function(err) { + error = err; + done(); + }); + }); + + it('should error', function() { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('something went wrong'); + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should not regenerate session', function() { + expect(req.session.id).to.equal('1'); + }); + + it('should not set user', function() { + expect(req.user).to.be.null; + }); + + it('should not serialize user', function() { + expect(req.session['passport'].user).to.be.undefined; + }); + }); + describe('encountering an error when serializing to session', function() { var passport = new Passport(); passport.serializeUser(function(user, done) { @@ -299,8 +500,12 @@ describe('http.ServerRequest', function() { req._passport = {}; req._passport.instance = passport; req._sessionManager = passport._sm; - req.session = {}; + req.session = { id: '1' }; req.session['passport'] = {}; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + process.nextTick(cb); + } var error; @@ -323,12 +528,75 @@ describe('http.ServerRequest', function() { expect(req.isUnauthenticated()).to.be.true; }); + it('should regenerate session', function() { + expect(req.session.id).to.equal('2'); + }); + it('should not set user', function() { expect(req.user).to.be.null; }); it('should not serialize user', function() { - expect(req.session['passport'].user).to.be.undefined; + expect(req.session['passport']).to.be.undefined; + }); + }); + + describe('encountering an error when saving session', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session['passport'] = {}; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + req.session.save = function(cb) { + process.nextTick(function(){ + cb(new Error('something went wrong')); + }); + }; + process.nextTick(cb); + } + + var error; + + before(function(done) { + var user = { id: '1', username: 'root' }; + + req.login(user, function(err) { + error = err; + done(); + }); + }); + + it('should error', function() { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('something went wrong'); + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should not regenerate session', function() { + expect(req.session.id).to.equal('2'); + }); + + it('should not set user', function() { + expect(req.user).to.be.null; + }); + + it('should not serialize user', function() { + expect(req.session['passport'].user).to.equal('1'); }); }); @@ -369,6 +637,35 @@ describe('http.ServerRequest', function() { }); }); + describe('establishing a session without session support', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.login = request.login; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + + var error; + + before(function(done) { + var user = { id: '1', username: 'root' }; + + req.login(user, function(err) { + error = err; + done(); + }); + }); + + it('should error', function() { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('Login sessions require session support. Did you forget to use `express-session` middleware?'); + }); + }); + }); @@ -385,11 +682,81 @@ describe('http.ServerRequest', function() { req._passport = {}; req._passport.instance = passport; req._sessionManager = passport._sm; - req.session = {}; + req.session = { id: '1' }; req.session['passport'] = {}; req.session['passport'].user = '1'; + req.session.save = function(cb) { + expect(req.session['passport'].user).to.be.undefined; + process.nextTick(cb); + }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + process.nextTick(cb); + }; - req.logout(); + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should clear user', function() { + expect(req.user).to.be.null; + }); + + it('should clear serialized user', function() { + expect(req.session['passport']).to.be.undefined; + }); + }); + + describe('existing session and not keeping session data', function() { + var passport = new Passport(); + + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req.user = { id: '1', username: 'root' }; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { cart: [ '1', '2', ] }; + Object.defineProperty(req.session, 'id', { value: '1' }); + req.session['passport'] = {}; + req.session['passport'].user = '1'; + req.session.save = function(cb) { + expect(req.session['passport'].user).to.be.undefined; + process.nextTick(cb); + }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + process.nextTick(cb); + }; + + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); it('should not be authenticated', function() { expect(req.isAuthenticated()).to.be.false; @@ -401,7 +768,66 @@ describe('http.ServerRequest', function() { }); it('should clear serialized user', function() { + expect(req.session['passport']).to.be.undefined; + }); + + it('should keep session data', function() { + expect(req.session.cart).to.be.undefined; + }); + }); + + describe('existing session and keeping session data', function() { + var passport = new Passport(); + + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req.user = { id: '1', username: 'root' }; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { cart: [ '1', '2', ] }; + Object.defineProperty(req.session, 'id', { value: '1' }); + req.session['passport'] = {}; + req.session['passport'].user = '1'; + req.session.save = function(cb) { expect(req.session['passport'].user).to.be.undefined; + process.nextTick(cb); + }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + process.nextTick(cb); + }; + + var error; + + before(function(done) { + req.logout({ keepSessionInfo: true }, function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should clear user', function() { + expect(req.user).to.be.null; + }); + + it('should clear serialized user', function() { + expect(req.session['passport'].user).to.be.undefined; + }); + + it('should keep session data', function() { + expect(req.session.cart).to.deep.equal([ '1', '2' ]); }); }); @@ -417,11 +843,30 @@ describe('http.ServerRequest', function() { req._passport.instance = passport; req._userProperty = 'currentUser'; req._sessionManager = passport._sm; - req.session = {}; + req.session = { id: '1' }; req.session['passport'] = {}; req.session['passport'].user = '1'; + req.session.save = function(cb) { + expect(req.session['passport'].user).to.be.undefined; + process.nextTick(cb); + }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + process.nextTick(cb); + }; - req.logout(); + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); it('should not be authenticated', function() { expect(req.isAuthenticated()).to.be.false; @@ -433,7 +878,7 @@ describe('http.ServerRequest', function() { }); it('should clear serialized user', function() { - expect(req.session['passport'].user).to.be.undefined; + expect(req.session['passport']).to.be.undefined; }); }); @@ -456,6 +901,190 @@ describe('http.ServerRequest', function() { }); }); + describe('existing session, without passport.initialize() middleware, and invoked with a callback', function() { + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req.user = { id: '1', username: 'root' }; + + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should clear user', function() { + expect(req.user).to.be.null; + }); + }); + + describe('encountering an error saving existing session', function() { + var passport = new Passport(); + + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req.user = { id: '1', username: 'root' }; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session['passport'] = {}; + req.session['passport'].user = '1'; + req.session.save = function(cb) { + expect(req.session['passport'].user).to.be.undefined; + process.nextTick(function() { + cb(new Error('something went wrong')); + }); + }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + process.nextTick(cb); + }; + + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should error', function() { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('something went wrong'); + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should clear user', function() { + expect(req.user).to.be.null; + }); + + it('should clear serialized user', function() { + expect(req.session['passport'].user).to.be.undefined; + }); + }); + + describe('encountering an error regenerating session', function() { + var passport = new Passport(); + + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req.user = { id: '1', username: 'root' }; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session['passport'] = {}; + req.session['passport'].user = '1'; + req.session.save = function(cb) { + expect(req.session['passport'].user).to.be.undefined; + process.nextTick(cb); + }; + req.session.regenerate = function(cb) { + process.nextTick(function() { + cb(new Error('something went wrong')); + }); + }; + + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should error', function() { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('something went wrong'); + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should clear user', function() { + expect(req.user).to.be.null; + }); + + it('should clear serialized user', function() { + expect(req.session['passport'].user).to.be.undefined; + }); + }); + + describe('existing session, but not passing a callback argument', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.logout = request.logout; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = {}; + req.session['passport'] = {}; + req.session['passport'].user = '1'; + + it('should throw an exception', function() { + expect(function() { + req.logout(); + }).to.throw(Error, 'req#logout requires a callback function'); + }); + }); + + describe('without session support', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.logout = request.logout; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should error', function() { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('Login sessions require session support. Did you forget to use `express-session` middleware?'); + }); + }); + });