Sharing sessions between SocketIO and Express using Redis
Development | Tihomir Kit

Sharing sessions between SocketIO and Express using Redis

Monday, Aug 25, 2014 • 6 min read
In this post I'll explain how to share session data between SocketIO and NodeJS Express framework by storing session data to Redis. You can download the showcase app made to go together with the post from GitHub.

Introduction

In this post I’ll explain how to share session data between SocketIO and NodeJS Express framework. This will allow you to access and modify the same session data through both standard REST API calls and sockets communication.

To make our solution a bit more robust, we’ll add Redis to the mix. This will allow us to persist the session data even if our server application stops for whatever reason. After the server comes back up again, all our session data will still be there.

Why do we need this? Even if you’re using SocketIO, chances are you don’t really need all your communication between the client and the server to go through sockets. For all the parts of your application that require real-time communication SocketIO will be perfect but for everything else you might be better off using standard REST API calls. Why? Without going into “speed” and “ease of implementation” pros and cons (there’s plenty of that lying around the Internet) I see a few reasons why one might want to use both in the same application. REST is still a bit more “standard” way to exchange data and if there is no real need to use websockets everywhere, your team might be a bit more comfortable using REST since it’s what they’ve been using “ever since”. Other than that, you might be in the middle of a project that previously used REST for everything and now the need arose to develop a component which requires real-time communication. Of course, you won’t be rewriting all your code to SocketIO because there are more important things to dedicate your time to. Or you might simply believe SocketIO should be in charge of everything real-time and REST should be used to handle everything else - which is also perfectly fine. Either way, you’ll most probably get into situation where you’ll want to share session data between the two.

The showcase app I made to go with the post is a very simple one. It lets the user enter a username and change it afterwards upon successful registration. Both of these actions are broadcasted to all the other users. To prove that session sharing is working the first request (registration) is made through a REST route and the second one (rename) through SocketIO. During the first request the userName is stored to session data which is seamlessly stored to Redis and during the second request the session is obtained from Redis, userName is updated and then the session is stored once again. The client side of the application is made in AngularJS but it only acts as a simple wrapper around SocketIO so I believe that shouldn’t be a problem. Since the focus of this post is on sharing sessions between Express and SocketIO, only the most important parts of the code will be included in the post while you can check the rest of the code on GitHub.

The app was made using NodeJS Express v4.8.5, SocketIO v1.0.6 and Redis v2.8.12. I also made a branch which uses NodeJS Express v3.5.1 and SocketIO v0.9.16 which is actually not that much different but some code adjustments had to be made to make it work. This blog post will focus on Express v4 and SocketIO v1.

Setup

To setup the application, you’ll need to do the following:

  1. Install Git Bash (Windows users only)
  2. Install NodeJS
  3. Run InstallNpmDependencies.sh located in the root directory of the project
    • this will globally install all the needed npm and bower dependencies for both SESIOR.Node (server-side) and SESIOR.SPA (client-side)
    • if you don’t want npm to install the dependencies globally, check the dependencies in InstallNpmDependencies.sh and install them manually
  4. Install Redis (for Windows users)
  5. Setup SESIOR.Node/config.js if needed

Running the application

  1. Start redis-server (for Windows users redis-X.Y/bin/release/redis-server.exe from the downloaded zip)
  2. Run RunNode.sh to start the SESIOR.Node portion of the application (uses Nodemon)
  3. Run RunSPA.sh to start the SESIOR.SPA portion of the application
  4. If you’re running Windows, you can add SESIOR.SPA/app to IIS and run it that way
  • make sure to set your SESIOR.SPA hostname in SESIOR.Node/config.js as allowed CORS origins

Code

var config = require('../config');
 
var redisClient = null;
var redisStore = null;
 
var self = module.exports = {
    initializeRedis: function (client, store) {
        redisClient = client;
        redisStore = store;
    },
    getSessionId: function (handshake) {
        return handshake.signedCookies[config.sessionCookieKey];
    },
    get: function (handshake, callback) {
        var sessionId = self.getSessionId(handshake);
 
        self.getSessionBySessionID(sessionId, function (err, session) {
            if (err) callback(err);
            if (callback != undefined)
                callback(null, session);
        });
    },
    getSessionBySessionID: function (sessionId, callback) {
        redisStore.load(sessionId, function (err, session) {
            if (err) callback(err);
            if (callback != undefined)
                callback(null, session);
        });
    },
    getUserName: function (handshake, callback) {
        self.get(handshake, function (err, session) {
            if (err) callback(err);
            if (session)
                callback(null, session.userName);
            else
                callback(null);
        });
    },
    updateSession: function (session, callback) {
        try {
            session.reload(function () {
                session.touch().save();
                callback(null, session);
            });
        }
        catch (err) {
            callback(err);
        }
    },
    setSessionProperty: function (session, propertyName, propertyValue, callback) {
        session[propertyName] = propertyValue;
        self.updateSession(session, callback);
    }
};

Now for the app.js dependencies. Loading the modules properly will tie up express, express-session, socket.io, redis and connect-redis modules. This part is quite important since RedistStore for example depends on the express-session module. It requires express-session to make the Redis work with Express sessions seamlessly without us having to worry about Redis at all (upon receiving REST requests, this will enable us to modify session objects as if they were any other JSON objects). Here is the code:

// Module dependencies
var express = require('express');
var session = require('express-session');
var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');
var errorHandler = require('errorhandler');
 
var http = require('http');
var path = require('path');
var io = require('socket.io');
 
var app = express();
var server = http.createServer(app);
var ioServer = io.listen(server);
 
var redis = require('redis');
var redisClient = redis.createClient();
var RedisStore = require('connect-redis')(session);
var redisStore = new RedisStore({ client: redisClient });
 
// Other
var config = require('./config');
var sessionService = require('./shared/session-service');
sessionService.initializeRedis(redisClient, redisStore);
 
// Socket route dependencies
var usersSocketRoute = require('./routes/users-socket');

The following settings will allow Cross-Origin-Resource-Sharing which is a requirement since the client-side will be served on a different port from the server-side:

// Enable CORS
var allowCrossDomain = function (req, res, next) {
    res.header("Access-Control-Allow-Origin", config.allowedCORSOrigins);
    res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE");
    res.header("Access-Control-Allow-Headers", "Content-Type");
    res.header("Access-Control-Allow-Credentials", "true");
    next();
};
 
app.use(allowCrossDomain);

We also need to setup the session module to use redistStore for storing sessions. Both the cookie-parser and session modules need to use the same sessionSecret string so cookies and session data could be bound together. Keep in mind that the session cookie only contains the sessionId and all the session data is stored on the server. This both saves bandwidth and prevents any malicious session tampering on the client-side. The sessionCookieKey sets the name of the session cookie (“connect.sid” is used).

app.use(cookieParser(config.sessionSecret));
app.use(session({
    store: redisStore,
    key: config.sessionCookieKey,
    secret: config.sessionSecret,
    resave: true,
    saveUninitialized: true
}));

Now for the tricky part. This part of the code will get executed each time a client initiates a SocketIO connection with the server. That is a moment upon which we can read the session from the socket handshake and add it to the handshake so we could use it later in our custom socket modules/routes more easily (we won’t need to re-parse it on each client socket emit).

ioServer.use(function (socket, next) {
    var parseCookie = cookieParser(config.sessionSecret);
    var handshake = socket.request;
 
    parseCookie(handshake, null, function (err, data) {
        sessionService.get(handshake, function (err, session) {
            if (err)
                next(new Error(err.message));
            if (!session)
                next(new Error("Not authorized"));
 
            handshake.session = session;
            next();
        });
    });
});

This is an example of initiating a socket module/route. Notice the passing of the socket object which contains our previously set handshake session.

ioServer.sockets.on('connection', function (socket) {
    usersSocketRoute(socket);
});

Now in our user-socket-route.js we can access and modify sessions (socket.request.session) very easily by doing something like this:

var sessionService = require('../shared/session-service');
var membershipRoute = require('../routes/membership');
 
module.exports = function (socket) {
    // ...
 
    socket.on("user:rename", function (userName, callback) {
        sessionService.getUserName(socket.request, function (err, oldUserName) {
            sessionService.setSessionProperty(socket.request.session, "userName", userName, function (err, data) {
                if (err) {
                    callback(err);
                    return;
                }
 
                sessionService.getUserName(socket.request, function (err, newUserName) {
                    if (err) {
                        callback(err);
                        return;
                    }
 
                    membershipRoute.renameUser(oldUserName, newUserName);
                    var user = membershipRoute.getUser(newUserName);
                    var data = { oldUserName: oldUserName, user: user };
 
                    socket.broadcast.emit("user:renamed", data);
                    callback(null, data);
                });
            });
        });
    });
 
    // ...
};

This last part of the code reads the userName from the socket.request.session, and then updates the session after which it re-reads it again. If you change your username in the browser and hit F5 to refresh the page, you’ll notice that the new username is persisted. In a real case scenario, there would be no need to read the userName through the sessionService as you could easily access it through socket.request.session.userName but I did this in a more complicated way as a proof-of-concept - to prove we actually read the data from the session and not from a module closure. In regular REST requests you can access and modify sessions by simply accessing req.session.whatever (like I did in membership.js module).

Conclusion

Sharing sessions between Express and SocketIO might be a bit tricky to figure out, but once you set it up it will be a breeze to use it. For the rest of the code, please check the GitHub repo.

Enjoy!