MEANJS: Security in SocketIO

2019-07-22 04:30发布

问题:

Situation

I'm using the library SocketIO in my MEAN.JS application.

in NodeJS server controller:

    var socketio = req.app.get('socketio');
    socketio.sockets.emit('article.created.'+req.user._id,  data);

in AngularJS client controller:

   //Creating listener        
   Socket.on('article.created.'+Authentication.user._id, callback);

   //Destroy Listener
   $scope.$on('$destroy',function(){
        Socket.removeListener('article.created.'+Authentication.user._id, callback);
   });

Okey. Works well...

Problem

If a person (hacker or another) get the id of the user, he can create in another application a listener in the same channel and he can watch all the data that is sends to the user; for example all the notificacions...

  • How can I do the same thing but with more security?

Thanks!

回答1:

Some time ago I stumbled upon the very same issue. Here's my solution (with minor modifications - used in production).

We will use Socket.IO namespaces to create private room for each user. Then we can emit messages (server-side) to specific rooms. In our case - only so specific user can receive them.

But to create private room for each connected user, we have to verify their identify first. We'll use simple piece of authentication middleware for that, supported by Socket.IO since its 1.0 release.

1. Authentication middleware

Since its 1.0 release, Socket.IO supports middleware. We'll use it to:

  1. Verify connecting user identify, using JSON Web Token (see jwt-simple) he sent us as query parameter. (Note that this is just an example, there are many other ways to do this.)
  2. Save his user id (read from the token) within socket.io connection instance, for later usage (in step 2).

Server-side code example:

var io = socketio.listen(server); // initialize the listener

io.use(function(socket, next) {
  var handshake = socket.request;
  var decoded;

  try {
    decoded = jwt.decode(handshake.query().accessToken, tokenSecret);
  } catch (err) {
    console.error(err);
    next(new Error('Invalid token!'));
  }
  if (decoded) {
    // everything went fine - save userId as property of given connection instance
    socket.userId = decoded.userId; // save user id we just got from the token, to be used later
    next();
  } else {
    // invalid token - terminate the connection
    next(new Error('Invalid token!'));
  }
});

Here's example on how to provide token when initializing the connection, client-side:

socket = io("http://stackoverflow.com/", {
  query: 'accessToken=' + accessToken
});

2. Namespacing

Socket.io namespaces provide us with ability to create private room for each connected user. Then we can emit messages into specific room (so only users within it will receive them, as opposed to every connected client).

In previous step we made sure that:

  1. Only authenticated users can connect to our Socket.IO interface.
  2. For each connected client, we saved user id as property of socket.io connection instance (socket.userId).

All that's left to do is joining proper room upon each connection, with name equal to user id of freshly connected client.

io.on('connection', function(socket){
  socket.join(socket.userId); // "userId" saved during authentication

  // ...
});

Now, we can emit targeted messages that only this user will receive:

io.in(req.user._id).emit('article.created', data); // we can safely drop req.user._id from event name itself