Simple JWT authentication for SocketIO
This post is a follow-up to my somewhat rambling earlier post here. I would still say it’s worth reading the earlier post to get an idea of some of the complexities and uncertainties around sockets and authentication, but I do go on a bit. For some actual code please read on.
My socket authentication technique developed from the angular-fullstack generator implementation. However I removed the server-side dependency for the Auth0.com [socketio-jwt] (https://github.com/auth0/socketio-jwt) module.
This module and other blog posts on the Auth0.com site provided me with a lot of useful info and guidance, but I ultimately preferred not to use their socketio module for the following reasons:
-
it abstracts away too much of what is going on so I don’t have fine-grained control of the process
-
along with the angular-fullstack generated server code, it seems to favour the technique of passing a JWT token in the query string when the client socket connects to the server for the first time. This is arguably more insecure than passing the token in the body of a socket message
However i am still using Auth0’s jsonwebtoken node module for authenticating the actual JWT token.
It is not possible to send data in the body of the connection
event for a socket. So to authenticate with a token in the socket body requires two round trips at the beginning. This approach means that the socket client connects with io.connect()
and then is temporarily disabled from receiving messages on the server side until the client sends a second authenticate
message to the server with a JWT token in the body. Once the token is authenticated the socket is re-enabled and can be used in the normal way.
I got the idea of this technique from this post:
https://facundoolano.wordpress.com/2014/10/11/better-authentication-for-socket-io-no-query-strings/
The facundoolano post shows a ‘temporary disabling’ socket technique for a server with multiple rooms/namespaces. My demo below will just show a technique for a socket server with one default namespace. But I recommend reading the facundoolano post for a more comprehensive way of doing the ‘two-round-trips’ socket auth technique.
Server side code
var jwt = require('jsonwebtoken');
module.exports = function (socketio) {
socketio.on('connection', function(socket){
//temp delete socket from namespace connected map
delete socketio.sockets.connected[socket.id];
var options = {
secret: process.env.SESSION_SECRET,
timeout: 5000 // 5 seconds to send the authentication message
}
var auth_timeout = setTimeout(function () {
socket.disconnect('unauthorized');
}, options.timeout || 5000);
var authenticate = function (data) {
clearTimeout(auth_timeout);
jwt.verify(data.token, options.secret, options, function(err, decoded) {
if (err){
socket.disconnect('unauthorized');
}
if (!err && decoded){
//restore temporarily disabled connection
socketio.sockets.connected[socket.id] = socket;
socket.decoded_token = decoded;
socket.connectedAt = new Date();
// Disconnect listener
socket.on('disconnect', function () {
console.info('SOCKET [%s] DISCONNECTED', socket.id);
});
console.info('SOCKET [%s] CONNECTED', socket.id);
socket.emit('authenticated');
}
})
}
socket.on('authenticate', authenticate );
});
};
Client side code in Angular
NB I am wrapping the client socket connection in an initialize
method to control when the client connects to the socket server, i.e not until after the user has logged in to the app via a REST API auth process. Then the socket auth can use the same JWT token as the app auth.
/* global io */
angular.module('myApp')
.factory('socket', function(socketFactory, Auth) {
var socket, ioSocket, isAuthenticated,
self = {
getAuthenticated:function(){
return isAuthenticated;
}
};
// by default the socket property is null and is not authenticated
self.socket = socket;
// initializer function to connect the socket for the first time after logging in to the app
self.initialize = function(){
console.log('initializing socket');
isAuthenticated = false;
// socket.io now auto-configures its connection when we omit a connection url
ioSocket = io('', {
path: '/socket.io-client'
});
//call btford angular-socket-io library factory to connect to server at this point
self.socket = socket = socketFactory({
ioSocket: ioSocket
});
//---------------------
//these listeners will only be applied once when socket.initialize is called
//they will be triggered each time the socket connects/re-connects (e.g. when logging out and logging in again)
//----------------------
socket.on('authenticated', function () {
isAuthenticated = true;
console.log('socket is jwt authenticated');
});
//---------------------
socket.on('connect', function () {
//send the jwt
socket.emit('authenticate', {token: Auth.getToken()});
});
};
return self;
})
Finally here’s an Angular socketAuth
service with a promise-returning method for triggering the first client socket connection and checking if the socket is authenticated. E.g. for use when resolving a state change to a state that will display socket messages.
angular.module('myApp')
.factory('socketAuth', function(socket, $q) {
return {
getAuthenticatedAsPromise:function(){
var listenForAuthentication = function(){
console.log('listening for socket authentication');
var listenDeferred = $q.defer();
var authCallback = function(){
console.log('listening for socket authentication - done');
listenDeferred.resolve(true);
};
socket.socket.on('authenticated', authCallback);
return listenDeferred.promise;
};
if(!socket.socket){
socket.initialize();
return listenForAuthentication();
}else{
if(socket.getAuthenticated()){
return $q.when(true);
}else{
return listenForAuthentication();
}
}
}
};
})