Sharing Sessions and Authentication Between Rails and Node.js Using Redis
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'];
... ...
});