2

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!

Community
  • 1
  • 1
Aral Roca
  • 5,442
  • 8
  • 47
  • 78

1 Answers1

2

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
bardzusny
  • 3,788
  • 7
  • 30
  • 30
  • Thank you first, because is a awesome answer! I have questions now... In client side code example are the variable "tokenSecret", what is it? what I need to put in this variable? And the same with the variable "accessToken" in client-side code example. Sorry about that, I never used JSON Web Token before. Thank you again! – Aral Roca Jul 08 '15 at 09:17
  • By the way, I'm using AngularJS in client-side. I need to install with bower any JWT library for provide the accessToken? Thanks – Aral Roca Jul 08 '15 at 09:20
  • Yes, I was a little worried that perhaps I should explain those two a little more, but stopped myself because it seemed a little "off-topic". tl;dr: `tokenSecret` can be any string, the longer and more random the better. This is what server will use to both encode and decode tokens, so **never-ever** share it with a client. `accessToken` is something you have to get from the server first. You should give it username, password, and get back token generated using `jtw.encode()`, with payload containing `userId` of the freshly authenticated user (for example `{ userId: ... }`). – bardzusny Jul 08 '15 at 09:30
  • If that's not enough I'll try to edit the answer to include important/missing bit or two, hopefully not going into any real digressions. Perhaps (while I'm not recommending it myself) it would be simpler to pass username and password instead of JTW token when connecting with `socket.io`, and then load full user object, based on username/password combination, all within authentication middleware I provided (only a little modified)? (I see that you're using Mongoose/MongoDB.) – bardzusny Jul 08 '15 at 09:33
  • Okey, I tried to understand it... I put the variable **tokenSecret** like this: `var tokenSecret = '570ee3818635829e35700b0166ccfec8';` _(invented)_ in the **server-side**. The **payload** is the **accessToken** that we send in **client-side** and receive in the server in `socket.request.query().accessToken`, isn't it? Is this payload encoded, right? If we never can share the tokenSecret with any client, where we encode the payload? By the way, exists this error: `[TypeError: Object # has no method 'query']` with **.query()** method. Thank you. – Aral Roca Jul 08 '15 at 10:24
  • Okey... Perhaps a good method is to encode with jwt the **userId** when the users sign in to the server (in server side, using the same tokenSecret), and send to the client with `res.json({token: token});` and, then, the accesToken is this token? Thanks again! – Aral Roca Jul 08 '15 at 10:36
  • @AralRoca, exactly as in your last comment. Then you can actually use it for authentication, as opposed to sending login/password with each request. Passport.JS (I see `req.user` object, perhaps you're using it already?) even provides ready-made authentication strategy just for that, called `Bearer` strategy. Glad you figured it out! – bardzusny Jul 08 '15 at 10:58
  • yes, I'm using `req.user` object. I'm using this framework: [MeanJS](https://github.com/meanjs/mean). In my **sign in** method I saved in the `$localStorage` variable _(AngularJS)_ the token, and then, in the factory of SocketIO in Angular I did this: `io.connect('http://localhost:8080',{query: 'accessToken=' + $localStorage.accessToken})`. Don't work... `[TypeError: Object # has no method 'query']` – Aral Roca Jul 08 '15 at 14:03
  • And I did a console.log of the token and is OK. And is encoded with the same tokenSecret... – Aral Roca Jul 08 '15 at 14:06
  • What `socket.io` version are you using? Most of the stuff I'm talking about is applicable to `1.0` and above. – bardzusny Jul 08 '15 at 14:21
  • I'm using `"socket.io": "^1.3.5"` – Aral Roca Jul 08 '15 at 14:29
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/82713/discussion-between-bardzusny-and-aral-roca). – bardzusny Jul 08 '15 at 14:41