9

I've created a simple Authentication application with passport (see code below). Express through the session middleware, creates a session on every request where the requesting client does not have a session already. I'd like to assign sessions only after logging or create a new session after login.

This is because I will eventually be doing login over HTTPS and would like to prevent hackers from hijacking the sessions from users that have authenticated.

Here is my server code:

// Server.js configures the application and sets up the webserver

//importing our modules
var express = require('express');
var app = express();
var port = process.env.PORT || 8080;
var mongoose = require('mongoose');
var passport = require('passport');
var flash = require('connect-flash');
var MongoStore = require('connect-mongo')(express);

var configDB = require('./config/database.js');

//Configuration of Databse and App

mongoose.connect(configDB.url); //connect to our database

require('./config/passport')(passport); //pass passport for configuration

app.configure(function() {

    //set up our express application

    app.use(express.logger('dev')); //log every request to the console
    app.use(express.cookieParser()); //read cookies (needed for auth)
    app.use(express.bodyParser()); //get info from html forms
    app.set('view engine', 'ejs'); //set up ejs for templating

    //configuration for passport
    app.use(express.session({ secret: 'olhosvermdfgytuelhoseasenhaclassica',
         cookie: {
            maxAge: 120000 },
         store:
             new MongoStore({
                db: 'xYrotr4h',
                host: 'novus.modulusmongo.net',
                port: 27017,
                username: 'gdog',
                password: 'fakepassowrd123'
            })
         })); //session secret + expiration + store
    app.use(passport.initialize());
    app.use(passport.session()); //persistent login session
    app.use(flash()); //use connect-flash for flash messages stored in session

});

//Set up routes
require('./app/routes.js')(app, passport);

//launch
app.listen(port);
console.log("Server listening on port" + port);

In my new Passport local strategy I tried to use req.session.regenerate() or req.session.reload() when the user was successfully validated against the database but that caused the server to crash.

Here is how I define my strategy:

//Passport.js sets up our local strategies

//imports

var LocalStrategy = require('passport-local').Strategy;
var User = require('../app/models/user');

//export this as a module since we give it to passport

module.exports = function(passport) {
    //Set up the session for persistent login

    passport.serializeUser(function(user, done) {
        done(null, user.id);
    });

    //used to serialize the user
    passport.deserializeUser(function(id, done) {
        User.findById(id, function(err, user) {
            done(err, user);
        });
    });

    //setting up local sign up

    passport.use('local-signup', new LocalStrategy({
            //by default, the local strategy uses usernames and password, we will override with email
            usernameField: 'email',
            passwordField: 'password',
            passReqToCallback: true
        },
        function(req, email, password, done) {
            console.log("Callback ran!");
            //asynchronous
            //User.findOne wont fire unless data is sent back
            process.nextTick(function() {
                console.log("I did run!");
                //find user whose email is the same as form email
                // we are checking to see if the user trying to sign up already exists
                User.findOne({ 'local.email': email }, function(err, user) {
                    //if there any errors, return the errors
                    if (err) {
                        return done(err);
                    }
                    //check to see if there any users already with that email
                    if (user) {
                        return done(null, false, req.flash('signupMessage', 'That email is already taken.'));
                    } else {
                        console.log('New user will be added to the DB!');
                        //if there is no user with that e-mail, create the user
                        var newUser = new User();

                        //we set the user's local credentials
                        newUser.local.email = email;
                        newUser.local.password = newUser.generateHash(password);

                        //save the user in the store
                        newUser.save(function(err) {
                            if (err) {
                                throw err;
                            }
                            return done(null, newUser);
                        });
                    }
                });
            });
        }));

        // =========================================================================
    // LOCAL LOGIN =============================================================
    // =========================================================================
    // we are using named strategies since we have one for login and one for signup
    // by default, if there was no name, it would just be called 'local'

    passport.use('local-login', new LocalStrategy({
        // by default, local strategy uses username and password, we will override with email
        usernameField : 'email',
        passwordField : 'password',
        passReqToCallback : true // allows us to pass back the entire request to the callback
    },
    function(req, email, password, done) { // callback with email and password from our form

        // find a user whose email is the same as the forms email
        // we are checking to see if the user trying to login already exists
        User.findOne({ 'local.email' :  email }, function(err, user) {
            // if there are any errors, return the error before anything else
            if (err)
                return done(err);

            // if no user is found, return the message
            if (!user)
                return done(null, false, req.flash('loginMessage', 'No user found.')); // req.flash is the way to set flashdata using connect-flash

            // if the user is found but the password is wrong
            if (!user.validPassword(password))
                return done(null, false, req.flash('loginMessage', 'Oops! Wrong password.')); // create the loginMessage and save it to session as flashdata

            // all is well, return successful user
            // removing the req.session.regenerate fixes any crashing
            req.session.regenerate(function(err, done, user){
                    return done(null, user);
                 });

        });

    }));

};
laggingreflex
  • 32,948
  • 35
  • 141
  • 196
Georges Krinker
  • 2,259
  • 4
  • 25
  • 24
  • If hackers can hijack the sessions from the users, can't they hijack the new generated sessions? – bnuhero Mar 06 '14 at 00:49
  • 1
    I believe they will at least have a hard time differentiating between an authenticated vs. unauthenticated session. And an authenticated session will go over HTTPS. – Georges Krinker Mar 06 '14 at 07:26

5 Answers5

21

After digging into passports and express-sessions library, I've figured it out!

var session = function (req, res) {
    var temp = req.session.passport; // {user: 1}
    req.session.regenerate(function(err){
        //req.session.passport is now undefined
        req.session.passport = temp;
        req.session.save(function(err){
            res.send(200);
        });
    });
};

app.post('/login', passport.authenticate('local'), session);

Basically I let passport do its authentication first, where it attaches an object onto req.session.passport. Passport uses this object to find out the mapping from session -> userId on further requests. When you regenerate the session, the req.session.passport object gets lost. Thus, you have to make sure to transfer it over to the newly generated session, and save it.

Steven Yang
  • 358
  • 3
  • 11
  • 2
    Awesome, Thanks ! This should be the answer ... I struggled so much on this problem, that I felt like writing my own `passport.js` :-) – Kaya Toast Dec 26 '14 at 18:39
  • This really should be the accepted answer; this is the exact underlying need. – delfuego Aug 01 '17 at 15:08
2

Disclosure: I'm the author of Passport.js.

As of passport@0.6.0, sessions are now regenerated automatically whenever a user logs in or logs out. There's no longer any need to implement this functionality at the application level.

The announcement has more information.

Wanted to update this discussion with the latest information for anyone else asking the same question.

Jared Hanson
  • 15,940
  • 5
  • 48
  • 45
  • Thanks! This helped me a lot. Waiting for the libraries that depend upon this to catch up. It looks like the change in `logout()` is causing some issues in other packages. So, will have to wait for those to catch up. Specifically, `passport-azure-ad`. – Curt Keisler May 25 '22 at 19:55
0

It looks like Jared doesn't want to support this directly based on issue #194 which I'm not sure I agree with - at the very least, passport should then expose its own session regenerate function. In any case, you can solve this generically by replacing:

req.session.regenerate(function(err, done, user){
    return done(null, user);
});

with something like this:

var passport = req._passport.instance;
req.session.regenerate(function(err, done, user) {
    req.session[passport._key] = {};
    req._passport.instance = passport;
    req._passport.session = req.session[passport._key];
    return done(null, user);
});
0

Real-life example from the Gitter.im Open-Source project: https://gitlab.com/gitlab-org/gitter/webapp/commit/44bb6d8934bce37b86d4ee3fcdba759967a5e5c1

Expanding on Steven Yang's answer:

In a case you've got multiple strategies with custom callbacks, you want to create a separate method (e.g. passportLogin) that will eventually call req.login:

//passportLogin.js

async function regeneratePassportSession(req) {
  const passportSession = req.session.passport;
  return new Promise((resolve, reject) =>
    req.session.regenerate(function(err) {
      if (err) reject(err);
      assert(!req.session.passport);
      req.session.passport = passportSession;
      req.session.save(function(err) {
        if (err) reject(err);
        resolve();
      });
    })
  );
}

/**
 * Adds user to passport, if this is the
 * first time (user just logged in) we generate a new session
 * and returns a user with identity object
 */
module.exports = async function passportLogin(req, user) {
  // if user just logged in (session hasn't been authenticated before)
  if (!req.user) await regeneratePassportSession(req);
  await new Promise((resolve, reject) => {
    req.login(user, err => {
      if (err) reject(err);
      resolve();
    });
  });
  return user;

And call this in every strategy.

UltraMaster
  • 1,054
  • 1
  • 11
  • 23
-1

I think you need to replace:

req.session.regenerate(function(err, done, user){
   return done(null, user);
});

with:

  req.login(user, function(err) {
    if (err) return res.status(500).send('error');
    return done(null,user); 
  });

The req.login internally calls your passport.serializeUser() function.

Guy
  • 65,082
  • 97
  • 254
  • 325