Junior
Junior

Reputation: 11988

How to access/save the session data on the authorization event in Socket.io/express-sessions?

I setup a websocket using Socket.io and express 4 framework on node.js server.

I am trying to implement authorization step for my users when using my websocket.

When a user connects, a token is passed as a query value to the server. At the server level, I query a database for a session that match the passed token. If a session is found, I do few other check to ensure that the token is not hijacked.

Problem

The session data seems to be cleared on every page reload. Or the server is failing to link the sessionId to the user who created it so everytime it generates a new session.

I am puzzled on how to access the session variables "if they are set."

My Code's Problem

When a user reload his/her page/client, the session data will become undefined on the new request. The session is good until the page is refreshed which is my problem. I need to be able to keep the session active even after the user refresh their page.

Questions

How can ensure that the session data are not cleared on every page refresh?

Here is my authorization code

io.set('authorization', function (handshakeData, accept) {

    var session = handshakeData.session || {};

    //This is always undefined!
    console.log('Session Data:' + session.icwsSessionId);

    //var cookies = handshakeData.headers.cookie;
    var token = handshakeData._query.tokenId || '';
    //console.log('Token: ' + token);

    if(!token){
        console.log('Log: token was not found');
        return accept('Token was found.', false);
    }

    //allow any user that is authorized
    if(session && session.autherized && token == session.token){
        console.log('Log: you are good to go');
        return accept('You are good to go', true);
    }

    //if the client changed their token "client logged out"
    //terminate the open session before opening a new one
    if (session.autherized && token != session.token){

        var icwsConnection = new icwsConn(icwsRequest);
        icwsRequest.setConnection(session.icwsServer, session.icwsPort);
        icwsRequest.setIcwsHeaders(session.icwsSessionId, session.icwsToken);
        icwsConnection.logout();

        session.autherized = false;
        session.token = null;
        session.icwsServer = null;
        session.icwsPort = null;
        session.icwsSessionId = null;
        session.icwsToken = null;

        icwsConnection = null;
    }

Here is my entire code if needed

var env = require('./modules/config'),
    app = require('express')(),
    https = require('https'),
    fs = require('fs'),
    session = require('express-session'),
    redisStore = require("connect-redis")(session),
    sharedsession = require("express-socket.io-session"),
    base64url = require('base64url');

const server = https.createServer(
    {
        key: fs.readFileSync('certs/key.pem'),
        cert: fs.readFileSync('certs/cert.pem')
    }, function (req, res){
        res.setHeader('Access-Control-Allow-Origin', '*');
        res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type');
    }
).listen(env.socket.port, env.socket.host, function () {
    console.log('\033[2J');
    console.log('Websocket is running at https://%s:%s', server.address().address, server.address().port);
});

var io = require('socket.io')(server);

const sessionMiddleware = session({
    store: new redisStore({
        host: env.redis.host,
        port: env.redis.port
    }),
    secret: env.session.secret,
    name: env.session.name,
    rolling: false,
    resave: true,
    saveUninitialized: true
});

app.use(sessionMiddleware);


// Use shared session middleware for socket.io
// setting autoSave:true
io.use(sharedsession(sessionMiddleware, {
    autoSave: true
})); 


var icwsReq = require('./modules/icws/request.js'),
    icwsConn = require('./modules/icws/connection.js'),
    icwsInter = require('./modules/icws/interactions.js'),
    sessionValidator = require('./modules/validator.js');

var clients = {};
var icwsRequest = new icwsReq();
var sessionChecker = new sessionValidator();



app.get('/', function (req, res) {
    res.send('welcome');
});

io.set('authorization', function (handshakeData, accept) {

    var session = handshakeData.session || {};

    //This is always undefined!
    console.log('Session Data:' + session.icwsSessionId);

    //var cookies = handshakeData.headers.cookie;
    var token = handshakeData._query.tokenId || '';
    //console.log('Token: ' + token);

    if(!token){
        console.log('Log: token was not found');
        return accept('Token was found.', false);
    }

    //allow any user that is authorized
    if(session && session.autherized && token == session.token){
        console.log('Log: you are good to go');
        return accept('You are good to go', true);
    }

    /*
    if (!originIsAllowed(origin)) {
        // Make sure we only accept requests from an allowed origin
        socket.destroy();
        console.log((new Date()) + ' Connection from origin ' + origin + ' rejected.');
        return false;
    }
    */

    //if the client changed their token "client logged out"
    //terminate the open session before opening a new one
    if (session.autherized && token != session.token){

        var icwsConnection = new icwsConn(icwsRequest);
        icwsRequest.setConnection(session.icwsServer, session.icwsPort);
        icwsRequest.setIcwsHeaders(session.icwsSessionId, session.icwsToken);
        icwsConnection.logout();

        session.autherized = false;
        session.token = null;
        session.icwsServer = null;
        session.icwsPort = null;
        session.icwsSessionId = null;
        session.icwsToken = null;

        icwsConnection = null;
    }

    var myIP = '10.0.4.195';

    var decodedToken = base64url.decode(token);

    sessionChecker.validateData(decodedToken, myIP, env.session.duration, function(isValid, icws){

        if(isValid){

            session.authorized = true;
            session.icwsServer = icws.host;
            session.icwsPort = icws.port;
            session.token = token;
            session.icwsSessionId = null;
            session.icwsToken = null;

            icwsRequest.setConnection(icws.host, icws.port);
            var icwsConnection = new icwsConn(icwsRequest);

            icwsConnection.login(icws.username, icws.password, function(isLogged, icwsSession, headers){

                if(isLogged && icwsSession.sessionId && icwsSession.csrfToken){

                    //icwsConnection.setWorkstation(icws.workstaton);
                    session.icwsSessionId = icwsSession.sessionId;
                    session.icwsToken = icwsSession.csrfToken;

                    icwsRequest.setIcwsHeaders(session.icwsSessionId, session.icwsToken);
                    console.log('Log: new connection to ICWS! ' + session.icwsSessionId );
                }

            });

            console.log('Log: new connection to websocket!')
            return accept('New connection to websocket!', true);

        } else {

            console.log('Log: token could not be validated!');
            return accept('Token could not be validated!', false);
        }

    });

});


io.on('connection', function (socket) { 


    console.log('Authorized Session! Websocket id ready for action!');
    //var origin = socket.request.headers.origin || '';
    //var myIP = socket.request.socket.remoteAddress || '';

    if(!socket.request.sessionID){
        console.log('Missing Session ID');
        return false;
    }

    var socketId = socket.id;
    var sessionID = socket.request.sessionID;


    //Add this socket to the user's connection
    if(userCons.indexOf(socketId) == -1){
        userCons.push(socketId);
    }

    clients[sessionID] = userCons;

    console.log(clients); //display all connected clients

    socket.on('placeCall', function(msg){

        icwsInter.call(method, uri, params, header, true);

    });

    socket.on('chat', function(msg){
        console.log('Chat Message: ' + msg);
        socket.emit('chat', { message: msg });
    });


    socket.on('disconnect', function(msg){
        console.log('Closing sessionID: ' + sessionID);
        var userCons = clients[sessionID] || [];

        var index = userCons.indexOf(socketId);

        if(index > -1){
            userCons.splice(index, 1);
            console.log('Removed Disconnect Message: ' + msg);
        } else {
            console.log('Disconnect Message: ' + msg);
        }

    }); 

    socket.on('error', function(msg){
        console.log('Error Message: ' + msg);
    }); 

});


function originIsAllowed(origin) {
    // put logic here to detect whether the specified origin is allowed.
        var allowed = env.session.allowedOrigins || []

        if(allowed.indexOf(origin) >= 0){
            return true;
        }

    return false;
}

Edited

The io cookie changes on every request. When a io cookie is created it will have a last accessed values of 12/31/1969 4:00:00 PM

Also, this cookie changes on every page reload.

After @Osk suggestion below Here is my new code which is still isn't saving my session data on page reload.

var env = require('./modules/config'),
    app = require('express')(),
    https = require('https'),
    fs = require('fs'),
    session = require('express-session'),
    redisStore = require("connect-redis")(session),
    sharedsession = require("express-socket.io-session"),
    base64url = require('base64url'),
    cookieParser = require("cookie-parser");

const server = https.createServer(
    {
        key: fs.readFileSync('certs/key.pem'),
        cert: fs.readFileSync('certs/cert.pem')
    }, function (req, res){
        res.setHeader('Access-Control-Allow-Origin', '*');
        res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type');
    }
).listen(env.socket.port, env.socket.host, function () {
    console.log('\033[2J');
    console.log('Websocket is running at https://%s:%s', server.address().address, server.address().port);
});

var io = require('socket.io')(server);
var sessionStore = new redisStore({
        host: env.redis.host,
        port: env.redis.port
    });

const sessionMiddleware = session({
        store: sessionStore,
        secret: env.session.secret,
        name: env.session.name,
        rolling: true,
        resave: false,
        saveUninitialized: false,
        cookie: { 
            maxAge: 60 * 60 * 1000
        }
    });


app.use(sessionMiddleware);

// Use shared session middleware for socket.io
// setting autoSave:true
io.use(sharedsession(sessionMiddleware, {
    autoSave: false
})); 

var icwsReq = require('./modules/icws/request.js'),
    icwsConn = require('./modules/icws/connection.js'),
    icwsInter = require('./modules/icws/interactions.js'),
    sessionValidator = require('./modules/validator.js');

var clients = {};

var icwsRequest = new icwsReq();
var sessionChecker = new sessionValidator();



app.get('/', function (req, res) {
    res.send('welcome');
});


//Middleware for authorizing a user before establishing a connection
io.use(function(socket, next) {


    var origin = socket.request.headers.origin || '';

    if (!originIsAllowed(origin)) {
        // Make sure we only accept requests from an allowed origin
        socket.destroy();
        console.log((new Date()) + ' Connection from origin ' + origin + ' rejected.');
        return false;
    }

    var myIP = socket.request.socket.remoteAddress || '';
    var token = socket.handshake.query.tokenId || '';
    var session = socket.handshake.session || {};

    //This should be defined on a reload
    console.log('IP Address: ' + myIP + '      SessionID: ' + socket.handshake.sessionID);

    if(!token){
        console.log('Log: token was not found');
        return next(new Error('Token not found'));
    }

    //allow any user that is authorized
    if(session && session.autherized && token == session.token){
        console.log('Log: you are good to go');
        return next(new Error('You are good to go'));
    }

    //if the client changed their token "client logged out"
    //terminate the open session before opening a new one
    if (session.autherized && token != session.token){

        var icwsConnection = new icwsConn(icwsRequest);
        icwsRequest.setConnection(session.icwsServer, session.icwsPort);
        icwsRequest.setIcwsHeaders(session.icwsSessionId, session.icwsToken);
        icwsConnection.logout();

        session.autherized = false;
        session.token = null;
        session.icwsServer = null;
        session.icwsPort = null;
        session.icwsSessionId = null;
        session.icwsToken = null;
        icwsConnection = null;
        session.save();
    }

    var decodedToken = base64url.decode(token);

    sessionChecker.validateData(decodedToken, myIP, env.session.duration, function(isValid, icws){

        if(isValid){

            session.authorized = true;
            session.icwsServer = icws.host;
            session.icwsPort = icws.port;
            session.token = token;
            session.icwsSessionId = null;
            session.icwsToken = null;

            icwsRequest.setConnection(icws.host, icws.port);
            var icwsConnection = new icwsConn(icwsRequest);
            /*
            icwsConnection.login(icws.username, icws.password, function(isLogged, icwsSession, headers){

                if(isLogged && icwsSession.sessionId && icwsSession.csrfToken){

                    //icwsConnection.setWorkstation(icws.workstaton);
                    session.icwsSessionId = icwsSession.sessionId;
                    session.icwsToken = icwsSession.csrfToken;

                    icwsRequest.setIcwsHeaders(session.icwsSessionId, session.icwsToken);
                    console.log('Log: new connection to ICWS! ' + session.icwsSessionId );
                }

            });
            */
            session.save(function(){
                console.log('Log: new connection to websocket!');   
            });

            return next();

        } else {

            console.log('Log: token could not be validated!');
            return next(new Error('Token could not be validated!'));
        }

    });

});


io.on('connection', function (socket) { 

    console.log('Connection is validated and ready for action!');

    var socketId = socket.id;

    if(!socket.handshake.sessionID){
        console.log('sessionId was not found');
        return false;
    }

    var sessionID = socket.handshake.sessionID;
    var userCons = clients[sessionID] || [];

    //Add this socket to the user's connection
    if(userCons.indexOf(socketId) == -1){
        userCons.push(socketId);
    }

    clients[sessionID] = userCons;

    //console.log(clients);

    socket.on('placeCall', function(msg){

        icws.call(method, uri, params, header, true);

    });

    socket.on('chat', function(msg){
        console.log('Chat Message: ' + msg);
        socket.emit('chat', { message: msg });
    });


    socket.on('disconnect', function(msg){
        console.log('Closing sessionID: ' + sessionID);
        var userCons = clients[sessionID] || [];

        var index = userCons.indexOf(socketId);

        if(index > -1){
            userCons.splice(index, 1);
            console.log('Removed Disconnect Message: ' + msg);
        } else {
            console.log('Disconnect Message: ' + msg);
        }

    }); 

    socket.on('error', function(msg){
        console.log('Error Message: ' + msg);
    }); 

});


function originIsAllowed(origin) {
    // put logic here to detect whether the specified origin is allowed.
        var allowed = env.session.allowedOrigins || []

        if(allowed.indexOf(origin) >= 0){
            return true;
        }

    return false;
}

Upvotes: 7

Views: 4363

Answers (2)

dievardump
dievardump

Reputation: 2503

Here is a working example of how you can share sessions between express and socket.io, even when they are not on the same domain.

(You can find a slightly different git repository with a running example here https://github.com/dievardump/express-socket-auth )

I simply use express-session, I don't see why using another middleware, since it works perfectly with socket.io

Since I do not have redis accessible, I used require('session-file-store') for the shared sessions.

Problem

Problem comes from the cross-domain policy which won't let you share the connect.sid Cookie value.

A workaround is :

  • serve non-httpOnly session cookies from the host (here for me server.dev). [express.js line 16]

  • read via JavaScript and send the connect.sid value as a sessionId parameter when connection to socket.io [client.js line 26:30]

  • when handshaking adding the value of connect.sid=socket.handshake.query.sessionId to the socket.handshake.headers.cookie before reading the handshake with the session middleware [socket.js line 32:37]

Architecture

Here followed :

  • server.js which require

    • express.js : Create express server accessed on my computer via http://server.dev:3000

      • serve HTML

      • create Sessions when loading page

    • socket.js : Create Socket.io server accessed on my computer via http://socket.dev:8000

  • client.js

    • served on http://server.dev:3000

    • connect to socket server on http://socket.dev:8000

Tests

Steps to test I used here :

  • Client open the page

  • If the cookie key connect.sid is not set

    • Client tries to connect to Socket.io : Connection error : [Not authenticated]

    • Client calls /authenticate

      • session is generated
    • Client tries to connect to Socket.io with value of connect.sid as sessionId parameter : Connection sucessfull

  • If cookie connect.sid is set

    • Client tries to connect to Socket.io with value of connect.sid as sessionId parameter : Connection sucessfull

Files

server.js

require('./express');
require('./socket');

express.js

    var express = require('express');
var app = express();
var http = require('http');
var io = require('socket.io');
var bodyParser = require('body-parser');
var sessionExpress = require('express-session');
var FileStore = require('session-file-store')(sessionExpress);

var secret = 'keyboard cat';
var session = sessionExpress({
    secret: secret,
    store: new FileStore(),
    resave: true,
    saveUninitialized: true,
    cookie: { 
        httpOnly: false, // important to allow client to read session cookie with JavaScript
        maxAge: 60 * 60 * 1000
    }
});

app.use(bodyParser.urlencoded({
    extended: true
}));
app.use(express.static(__dirname));

app.use('/authenticate', session);
app.get('/authenticate', function(req, res) {
    var session = req.session;
    session.userdata = session.userdata || {};
    session.userdata.connected = true;
    session.save(function(err) {
        if (err) {
            connectionError(res, session);
        } else {
            res.status(200);
            res.send();
        }
    });
});

// routes
app.get('/', function(req, res) {
    res.send('welcome');
});

// setup servers
var server = http.createServer(app);
server.listen(3000);

socket.js

var express = require('express');
var app = express();
var http = require('http');
var io = require('socket.io');
var sessionExpress = require('express-session');
var FileStore = require('session-file-store')(sessionExpress);

var secret = 'keyboard cat';
var sessionIdKey = 'connect.sid';

var session = sessionExpress({
    secret: secret,
    store: new FileStore(),
    resave: true,
    saveUninitialized: true,
    cookie: { 
        maxAge: 60 * 60 * 1000
    }
});

// setup servers
var server = http.createServer(app, function (req, res){
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type');
});

server.listen(8000);

var sio = io(server);
sio.use(function(socket, accept) {        
    // writing sessionId, sent as parameter, on the socket.handshake cookies
    if (socket.handshake.query.sessionId) {
        var cookies = (socket.handshake.headers.cookie || '').split(';');
        cookies.push(sessionIdKey + '=' + socket.handshake.query.sessionId);
        socket.handshake.headers.cookie = cookies.join(';');
    }
    session(socket.handshake, {}, function(err) {
        if (err) return accept(err);
        console.log('User trying to connect to Socket.io');
        var session = socket.handshake.session,
            userData = session.userdata || {};

        // is connected and good
        if (!userData || !userData.connected) {
            console.log('----- User has no active session, error');
            accept(new Error('Not authenticated'));
        } else {
            console.log('----- Socket.io connection attempt successful');
            accept(null, session.userid !== null);
        }
    });
});


sio.on('connection', function (socket) {
    console.log('Connection');
});

client.js

        function getCookie(name) {
            var value = "; " + document.cookie;
            var parts = value.split("; " + name + "=");
            if (parts.length == 2) return decodeURIComponent(parts.pop().split(";").shift());
        }

        function fetch(url, data, callback) {
            try {
                var x = new XMLHttpRequest();
                x.open(data ? 'POST' : 'GET', url, 1);
                x.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
                x.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
                x.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
                x.onreadystatechange = function () {
                    x.readyState > 3 && callback && callback(x.responseText, x);
                };
                x.send(data || null);
            } catch (e) {
                window.console && console.log(e);
            }
        };


        function connectServer(cb) {
            var sessionId = getCookie('connect.sid');
            var data = { forceNew: true, query : {  } };
            if (sessionId) {
                data.query.sessionId = sessionId
            }

            console.log('Trying to connect to Socket.io server');

            var server = io('http://socket.dev:8000', data);
            server.on('error', function (err) {
                console.log('----- Connection error : [%s]', err);
                setTimeout(function () {
                    cb();
                }, 200);
            });
            server.on('connect', function (data) {
                console.log('----- Connection successful with sessionId [%s]', sessionId);
                setTimeout(function () {
                    cb();
                }, 200);
            });
        }

        if (getCookie('connect.sid')) {
            console.log('Session cookie Detected');
            connectServer(function () { });
        } else {
            connectServer(function () {
                console.log('Call ./authenticate to create session server side');
                fetch('./authenticate', null, function () {
                    console.log('Session created')
                    connectServer(function () {});
                }); 
            }); 
        }

Execution

First Page loading Results

Client :

Trying to connect to Socket.io server
----- Connection error : [Not authenticated]
Call ./authenticate to create session server side
Session created
Trying to connect to Socket.io server
----- Connection successful with sessionId [s:Ir9dVPi8wzplPCoeNXAsDlOkhL8AW0gx.wwzUQ2jftntWEc6lRdYqGxRBoszjPtjT4dBW/KjFIXQ]

Server :

User trying to connect to Socket.io
----- User has no active session, error
User trying to connect to Socket.io
----- Socket.io connection attempt successful
Connection

Reload page

Client :

Session cookie Detected
Trying to connect to Socket.io server
----- Connection successful with sessionId [s:Ir9dVPi8wzplPCoeNXAsDlOkhL8AW0gx.wwzUQ2jftntWEc6lRdYqGxRBoszjPtjT4dBW/KjFIXQ]

Server :

User trying to connect to Socket.io
----- Socket.io connection attempt successful
Connection

Upvotes: 0

Osk
Osk

Reputation: 308

Which version of socket.io are you using?

express-socket.io-session works with socket.io 1.x

I see you're calling io.set() which is deprecated on socket.io 1.x

For more on this, take a look at http://socket.io/docs/migrating-from-0-9/ under the title Authentication differences. There, it's stated that

The old io.set() and io.get() methods are deprecated and only supported for backwards compatibility."

Could this be related to your issue ?

When you install the express-socket.io-session package, there's an example directory inside the package. It may come in handy to test against a working example for this module.

Upvotes: 2

Related Questions