I’m trying to implement a real-time notification feature to our Rails application. I finally chose Socket.IO which makes developing real-time event based communication systems rather simple. However, it turns out the real ‘bitching’ part is how to share sessions information and do authentication between rails and socket.io.

Firstly, you can not directly read rails’ session data from cookie in nodejs, because it’s encrypted. Secondly, even if you managed to use redis-store  to store sessions into redis, it’ll still fail to get the correct content. As discussed in this pull request, redis-store serialize data using Marshal.dump and Marshal.load, and nodejs is unable to recognize them. Node.js will always try to parse them as JSON. I’ve googled around and tried a marshal parser in javascript world, and even tried to replace Marshal with JSON in redis-store, but none of them ended up as an easy work and it caused some other issues to our existing Rails app.

So, finally, after some experiment, here is my workaround. Let Rails store session into cookie as default, no need to change it, however, I manually create a copy of session data in redis after user sign in, and expire them after user sign out. In order to achieve that, I override two methods provided by Devise inside of ApplicationController:

# app/controllers/application_controller.rb
def after_sign_in_path_for(resource_or_scope)
  #store session to redis
  if current_user
    # an unique MD5 key
    ukey = "#{session[:session_id]}:#{current_user.id}"
    cookies["_validation_token_key"] = Digest::MD5.hexdigest(ukey)
    # store session data or any authentication data you want here, generate to JSON data
    stored_session = JSON.generate({
      "user_id" => current_user.id,
      "username" => current_user.screen_name,
      ... ...
    })
    $redis.hset("mySessionStore", cookies["_validation_token_key"], stored_session)
   end
end

def after_sign_out_path_for(resource_or_scope)
  #expire session in redis
  if cookies["_validation_token_key"].present?
    $redis.hdel("mySessionStore", cookies["_validation_token_key"])
  end
end

When user first signs in, it creates a key called _validation_token_key and store it in cookies, the value of the key is an unique MD5 string. Meanwhile, this MD5 value is stored into a hash table called mySessionStore in redis using hset. Note that the corresponding value of this MD5 key in redis is some JSONed sessions data that you want to keep. When user sign out, Devise will call #after_sign_out_path_for method, and you can delete the hash key in redis at that time.

The key part of this code is the _validation_token_key stored in cookies, when user connects to Socket.IO, it will try to read this key from cookie, and then fetch the related session value from redis. You can easily use this to do client authentication across rails and nodejs. The example code on Node.js server side looks like this:

var io = require('socket.io').listen(3003);
var redis = require('redis');
var cookie = require("cookie"); // cookie parser
var redis_validate = redis.createClient(6379, "127.0.0.1");

io.configure(function (){
  io.set('authorization', function (data, callback) {

    if (data.headers.cookie) {

      data.cookie = cookie.parse(data.headers.cookie);
      data.sessionID = data.cookie['_validation_token_key'];

      // retrieve session from redis using the unique key stored in cookies
      redis_validate.hget(["mySessionStore", data.sessionID], function (err, session) {

        if (err || !session) {
          return callback('Unauthorized user', false);
        } else {
          // store session data in nodejs server for later use
          data.session = JSON.parse(session);
          return callback(null, true);
        }

      });

    } else {
      return callback('Unauthorized user', false);
    }
  });
});

... ...

After this, if the client passes the authentication, you can access session values through the connection callbacks:

io.sockets.on('connection', function(client){
  var user_id = client.handshake.session['user_id'];
  var username = client.handshake.session['username'];
  ... ...
});