Skip to content
Chaim Krause edited this page Oct 23, 2013 · 32 revisions

Authorization and handshaking

Socket.IO has support for 2 different authorization methods. Authorization can be applied globally which will be done during the initialization / handshaking process or it can be done per namespace.

Global and namespace level authorization can be used together or independent from each other. The only thing they have in common is the handshakeData object. The data object is generated with the request data from the handshake. So to have a better understanding of how to work with authorization you first need to understand the handshaking process.

Handshaking

When a client wants to establish a persistent real time connection with the Socket.IO server it needs to start a handshaking process. The handshake is initiated with either an XHR request or a JSONP request (for cross domain requests).

When the server receives a connection it will start gathering data from the request that might be needed later. This is done for two reasons:

  1. Users might want to authorize the clients based on information from the headers or IP address.
  2. Not all transports sends headers when they attempt to establish a real time connection with the server, so we store the handshake data internally so we are sure users can re-use this data again for when the client is connected. For example you might want to read out the session id from the cookie headers and initialize the Express session for the connected socket.

The handshakeData object contains the following information:

{
   headers: req.headers       // <Object> the headers of the request
 , time: (new Date) +''       // <String> date time of the connection
 , address: socket.address()  // <Object> remoteAddress and remotePort object
 , xdomain: !!headers.origin  // <Boolean> was it a cross domain request?
 , secure: socket.secure      // <Boolean> https connection
 , issued: +date              // <Number> EPOCH of when the handshake was created
 , url: request.url          // <String> the entrance path of the request
 , query: data.query          // <Object> the result of url.parse().query or a empty object
}

The address follows the API of socket.address()

After we fetch all information, we check if a global authorization function has been configured. If this is the case we pass it in authorization function the handshakeData object and a callback function. When the callback is called, we store the handshakeData internally so it can later be accessed during the connection event through the socket.handshake property. Based on the response of the callback we are going to either send a 403, 500 or a 200 success response.

#Global authorization Global authorization is enabled by setting the authorization configuration method.

io.configure(function (){
  io.set('authorization', function (handshakeData, callback) {
    callback(null, true); // error first callback style 
  });
});

Inside the authorization function above you receive 2 arguments:

  1. handshakeData, the handshakeData object that we generated during handshaking.
  2. callback, as handshakes might require database look-ups. It requires 2 arguments, error which can be undefined or a String in case of the error and authorized a Boolean indicating if the client is authorized.

Sending an error or setting the authorized argument to false both result in not allowing the client to connect to the server.

Because the handshakeData is stored after the authorization you can actually add or remove data from this object.

var io = require('socket.io').listen(80);

io.configure(function (){
  io.set('authorization', function (handshakeData, callback) {
    // findDatabyip is an async example function
    findDatabyIP(handshakeData.address.address, function (err, data) {
      if (err) return callback(err);

      if (data.authorized) {
        handshakeData.foo = 'bar';
        for(var prop in data) handshakeData[prop] = data[prop];
        callback(null, true);
      } else {
        callback(null, false);
      }
    }) 
  });
});

One small note, global and namespace based authorization share the same handshakeData object, so if you remove the headers or other important information from the handshakeData object, you wont be able to access them inside the namespace authentication. But also any data you add to the object will be available during the namespace authorization.

Not only the namespace has access to the handshakeData but also the connected socket. So any data you store during the handshake can be accessed during the connection event of a namespace by accessing the socket.handshake property as illustrated below:

io.sockets.on('connection', function (socket) {
  console.log(socket.handshake.foo == true); // writes `true`
  console.log(socket.handshake.address.address); // writes 127.0.0.1
});

##How does the client handle the global authorization When the authorization fails on the client side you can listen for the error event on the socket. You can also listen for that on the namespace, but there more error messages emitted there that could be related to errors in your namespace and not to the connection of socket.

For successful connections you can just keep listening for the connect event.

###Example

var sio = io.connect();

sio.socket.on('error', function (reason){
  console.error('Unable to connect Socket.IO', reason);
});

sio.on('connect', function (){
  console.info('successfully established a working connection \o/');
});

#Namespace authorization Authorization can also be per namespace, this might give you more flexibility for example you might have namespace for public usage such as chat box and have extra support chat box for registered members.

All the namespaces come with authorization method. This chainable method can be used to register a authorization method for the namespace. The function's arguments are identical to the global authorization function.

var io = require('socket.io').listen(80);

io.of('/private').authorization(function (handshakeData, callback) {
  console.dir(handshakeData);
  handshakeData.foo = 'baz';
  callback(null, true);
}).on('connection', function (socket) {
  console.dir(socket.handshake.foo);
});

##How does the client handle the namespace authorization Failed authorizations are handled a bit differently than global authorization requests. Instead of emitting an error event we emit a connect_failed event. But when the authorization passes the connect event will be emitted like you would expect.

###Example

var sio = io.connect()
  , socket = sio.socket;

socket.of('/example')
  .on('connect_failed', function (reason) {
    console.error('unable to connect to namespace', reason);
  })
  .on('connect', function () {
    console.info('sucessfully established a connection with the namespace');
  });