Search code examples
node.jsexpresscookiespassport.jsnode.js-connect

Node.js Express + Passport + Cookie Sessions n > 1 servers


From the Passport docs:

In a typical web application, the credentials used to authenticate a user will only be transmitted during the login request. If authentication succeeds, a session will be established and maintained via a cookie set in the user's browser.

Each subsequent request will not contain credentials, but rather the unique cookie that identifies the session. In order to support login sessions, Passport will serialize and deserialize user instances to and from the session.

So, being that the authentication is maintained in the client's browser, I should be able to scale the web servers to > 1 (each server uses same database) and have Passport deserialize user from an ID stored in a cookie regardless if the server has seen the user before. If the server has seen the user before but no cookie exists, I would expect Passport to fail the authentication...but I can't even get that far with n > 1 servers.

With 1 server process, Passport works fine:

The server is more or less verbatim of docs on both servers (after much trials w/ connect references and such):

app.configure(function() {
  app.use(express.static('public'));
  app.use(express.cookieParser());
  app.use(express.bodyParser());
  app.use(express.session({ secret: 'keyboard cat' }));
  app.use(passport.initialize());
  app.use(passport.session());
  app.use(app.router);
});

When a user login is successful, right before I return the HTTP request, I log req.user and req.session:

console.log(req.user);
console.log(req.session);
res.send(200, req.user);
// {"cookie":{
//   "originalMaxAge":null,
//   "expires":null,
//   "httpOnly":true,
//   "path":"/"
//  },
//  "passport":{"user":"ID"}}

When the HTTP request returns, the web app requests / and expects the server(s) to set req.user with ID ID from the cookie passed:

passport.deserializeUser(function(req, user_id, cb) {
  console.info('Passport deserializing user: '
    .concat(user_id));
  console.log(req.user);
  console.log(req.session);
  // ...
  // cb(null, user);
});

With 1 server running, this logs as:

Passport deserializing user: ID
undefined
undefined

Immediately after the callback cb(null, user) and before I return the request, I log req.user and req.session and see what I would expect!

Now, with n > 1 servers running, let's say 2, if the web app requests / from server 2 and expects it to set req.user with ID ID from the cookie passed (originally set by logging into server 1), I don't even see the deserialize function called! When I added logging to the beginning of the HTTP route (after middleware):

console.log(req.user);
console.log(req.session);

I see:

undefined
// {"cookie":{
//   "originalMaxAge":null,
//   "expires":null,
//   "httpOnly":true,
//   "path":"/"
//  },
//  "passport":{}}

...which tells me something is up with the cookie, but what? Both server processes are on Heroku behind the same DNS. I have read various discrepancies about needing some "shared session" store for this...but that's what a cookie is at the client's browser level. Please help! Express is the latest 3.X and Passport is 2.1.


Solution

  • The cookie does not contain session data, it contains just the session ID. Using this ID your Express application will get the related session data from it's session store. And since you are not defining a store when calling app.use(express.session({ secret: 'keyboard cat' })); you are using the built-in memory store which is not shared between processes.

    So, install any session store that uses a database, eg. connect-mongo for MongoDB.