The Use of Cookies Versus Query String Tokens to Identify Sessions in Socket.IO

May 11th, 2013 Permalink

For the purposes of this post let us say that you are building a single page web application that uses Socket.IO as a way to push messages from the server to connected clients via WebSockets or one of a range of fallback approaches for browsers lacking a WebSocket implementation. The opening stages of loading the application thus run as follows:

  • An HTTP/S request to load the application page.
  • HTTP/S requests to load assets - e.g. Javascript.
  • The client establishes a WebSocket or other Socket.IO connection.

Since the Socket.IO connection follows the page load, by that point a session has already been established on the web server. Most applications are stateful to the point of using sessions, and is usually the case that you want the Socket.IO code to have access to the current session. Two different scenarios are outlined in the rest of this post: using either (a) cookies or (b) query string tokens in order to allow Socket.IO code to pick up the client's session ID and thus load the session. For the sake of simplicity both cases assume the use of Express.js as the webserver so that all of the server-side code is Javascript under Node.js.

Setting up Sessions in Express.js

Setting up sessions in Express.js is straightforward. Here is code for a Redis-backed session store, for example. A client's session ID is stored encrypted in a cookie:

var express = require("express");
var redis = require("redis");
var RedisSessionStore = require("connect-redis")(express);

var app = express();
var cookieSecret = "cookie secret";
var cookieParser = express.cookieParser(cookieSecret);
var redisClient = redis.createClient(6379, "127.0.0.1");
var sessionStore = new RedisSessionStore({
  client: redisClient
});

app.use(cookieParser);
app.use(express.session({
  cookie: {
    httpOnly: true
  },
  key: "sid",
  secret: cookieSecret,
  store: sessionStore
}));

Session Identification via Cookies

Most of the online examples involving sharing of sessions between Socket.IO and Express.js use cookies and the Socket.IO authentication hook. When the initial upgrade HTTP/S request is made to establish a WebSocket connection the session cookies are passed over. This is also the case for most other fallback methods used by Socket.IO. These cookies are then accessible to whatever authentication function you put in place, and you can take that opportunity to load the session and associate it with the socket instance.

var http = require("http");
var io = require("socket.io");

var server = http.createServer(app).listen(10080);
var socketFactory = io.listen(server);
// Use the authorization hook to attach the session to the socket
// handshake by reading the cookie and loading the session when a
// socket connects. Using the authorization hook means that we can
// deny access to socket connections that arrive without a session - i.e.
// where the user didn't load a site page through Express.js first.
socketFactory.set("authorization", function (data, callback) {
  if (data && data.headers && data.headers.cookie) {
    cookieParser(data, {}, function (error) {
      if (error) {
        callback("COOKIE_PARSE_ERROR", false);
        return;
      }
      var sessionId = data.signedCookies[sessionKey];
      sessionStore.get(sessionId, function (error, session) {
        // Add the sessionId. This will show up in
        // socket.handshake.sessionId.
        //
        // It's useful to set the ID and session separately because of
        // those fun times when you have an ID but no session - it makes
        // debugging that much easier.
        data.sessionId = sessionId;
        if (error) {
          callback("ERROR", false);
        } else if (!session) {
          callback("NO_SESSION", false);
        } else {
          // Add the session. This will show up in
          // socket.handshake.session.
          data.session = session;
          callback(null, true);
        }
      });
    });
  } else {
    callback("NO_COOKIE", false);
  }
});

Thus wherever the socket object shows up, you now have access to the session and session ID. For example:

socketFactory.on("connection", function (socket) {
  console.log("Socket connection for session ID: " + socket.handshake.sessionId);
});

Disadvantages of Using Cookies

Some older mobile environments don't support cookies. This is thankfully much less of an issue now, but it is something to think about. Similarly, you never know when you might find yourself constrained to developing in an environment where cookies cannot be used. Again, not the usual circumstance, but worth bearing in mind.

The larger downside is something that you'll only see when it comes time to write test code. You will have to jump through a number of hoops in order make the Socket.IO client send cookies when running on the server. It can be done, but it isn't pretty: you might look at the Work Already package I put together recently that provides ready made web test tools for Socket.IO 0.9.*.

Session Identification via the Query String

The Socket.IO developers discourage the use of cookies for session identification, which is one of the reasons for the issues noted above - the developers aren't trying to support that use case, for all that it can be made to work. The preferred methodology is to pass some form of token with the query string when connecting from the client:

io.connect("/namespace?token=encrypted-string");

So if you're producing pages from templates in Express.js then you might set up middleware to add a suitable token to the locals:

express.use(function (req, res, next) {
  res.locals.socketIoNamespace = "namespace";
  res.locals.socketIoToken = someEncryptionFunction(req.session.id);
  next();
});

Then in whichever template is used for the Javascript that establishes the Socket.IO connection there will be something like the following Handlebars snippet:

io.connect("/{{socketIoNamespace}}?token={{socketIoToken}}");

Obtaining the session ID in the Socket.IO authentication function is then very similar to the cookie-reading version:

var http = require("http");
var io = require("socket.io");

var server = http.createServer(app).listen(10080);
var socketFactory = io.listen(server);
// Use the authorization hook to attach the session to the socket
// handshake by reading the token and loading the session when a
// socket connects. Using the authorization hook means that we can
// deny access to socket connections that arrive without a session - i.e.
// where the user didn't load a site page through Express.js first.
socketFactory.set("authorization", function (data, callback) {
  if (data && data.query && data.query.token) {
    var sessionId = someDecryptionFunction(data.query.token);
    sessionStore.get(sessionId, function (error, session) {
      // Add the sessionId. This will show up in
      // socket.handshake.sessionId.
      //
      // It's useful to set the ID and session separately because of
      // those fun times when you have an ID but no session - it makes
      // debugging that much easier.
      data.sessionId = sessionId;
      if (error) {
        callback("ERROR", false);
      } else if (!session) {
        callback("NO_SESSION", false);
      } else {
        // Add the session. This will show up in
        // socket.handshake.session.
        data.session = session;
        callback(null, true);
      }
    });
  } else {
    callback("NO_TOKEN", false);
  }
});