SocketCluster / socketcluster-client

JavaScript client for SocketCluster
MIT License
292 stars 91 forks source link

Auto-resubscribe fails when auth token expires or becomes invalid #11

Closed hedgepigdaniel closed 9 years ago

hedgepigdaniel commented 9 years ago

I'm having a problem where channels which are subscribed are not correctly resubscribed when a broken connection comes back up and the authentication token has become invalid. It can become invalid because it expires or because the socketcluster server was restarted.

If a client is authenticated and subscribed to a whole lot of channels requiring authentication when it is disconnected, if it is no longer authenticated when it reconnects then it will try and fail to resubscribe to all the channels it was subscribed to.

I think instead it should be possible to register a callback on the connect/reconnect event so that the client can reauthenticate before socketcluster automatically resubscribes to channels previously subscribed to.

Example client code '''javascript socket.on('connect', function(status, callback) { if (!status.isAuthenticated) { reAuthenticate(function(err) { callback(err); // Now the automatic resubscriptions can happen }); } else { callback(null); } }); '''

For background, our authentication system involves the browser doing an xmlhttp request to our webservers to get a temporary authentication token. This token is given to socketcluster with a socket.emit('login', token), and socketcluster then checks with our webservers that the token is valid. Since our webservers can't reliably revoke socketcluster's authentication token when the user logs out, the socketcluster auth token timeout is short. We also disconnect sockets after a period of browser inactivity to reduce server load, so this situation often arises when the browser becomes active and reconnects after the auth token has expired.

jondubois commented 9 years ago

@hedgepigdaniel Thanks for describing the problem.

Your suggested solution would definitely solve the problem. Its main drawback is that channels will not be resubscribed until that callback is called - So existing users of SC will need to alter their code to make sure that the callback gets called or else auto-resubscribe won't work at all (it adds an extra compulsory setup step on the client and breaks backwards compatibility).

It sounds like the problem boils down to whether or not developers are happy to use SC's auto-resubscribe flow or they need more control.

Maybe we can add an autoResubscribe [boolean] option to the client-side's socketCluster.connect(options) method to allow auto-resubscribe to be switched on/off?

Then we can expose a public socket.resubscribe() function which users could call whenever they like (but probably inside the 'connect' handler) - This would behave the same as your suggested solution (but instead of having to call a callback, developers can call socket.resubscribe()) - Also, it wouldn't beak backwards compatibility.


Some background info - SC's default authentication system was designed to support two common flows (I will put these up on the website at some point).

Flow 1:

  1. The client socket connects to the server
  2. The 'connect' event gets triggered on the client and, inside the handler function, the code checks for the status.isAuthenticated - If it is false, then the user will be shown a login screen.
  3. Once the user logs in, they will receive the authentication token with an expiry set to 1 hour from now.
  4. The server will now start sending a fresh token to that client socket every 50 minutes (example) so long as it's still connected.

^ Note that with this approach, the token will not expire so long as the socket is connected - You can think of this as the token being renewed at a set interval - It will only expire after the user has quit the app and their token is no longer being renewed. You still get the security benefit of having a short-lived token (so if a malicious entity manages to get hold of a token, it will only be useful for one hour).

Flow 2:

  1. The client socket connects to the server
  2. The 'connect' event gets triggered on the client and, inside the handler function, the code checks for the status.isAuthenticated - If it is false, then the user will be shown a login screen.
  3. Once the user logs in, they will receive the authentication token with an expiry set to 1 hour from now.
  4. After 1 hour, the token will have expired.
  5. If the socket loses the connection now, it will auto-reconnect and the 'connect' event will trigger on the client - status.isAuthenticated will be false and some channel resubscriptions will fail (all channels which require authentication will emit a 'subscribeFail' event and their state will become 'unsubscribed' - In this scenario, this is the expected behaviour; conceptually, the user lost their right to access those channels while in the middle their session).
  6. Now the user will be sent to the login screen - Once they have logged in, the channels will be manually resubscribed to as they were the first time round.

Note that as of the latest version of SC, you can provide a custom auth engine on the server - This allows you to control the status.isAuthenticated property which comes with the client socket's 'connect' event - This should allow you to integrate external token authentication solutions into SC. The documentation for how to do that is not yet up on the website.

jondubois commented 9 years ago

Also, to avoid the issue of all tokens being invalidated when you restart SC, you can provide a custom authKey option to the main SocketCluster() constructor inside server.js.

The reason they all get invalidated is because if you don't provide an authKey, SC will generate a 256-bit random key for you (it will be a different one each time you launch SC).

If you provide the same key each time, all previously granted tokens will still be valid... Until they expire.

hedgepigdaniel commented 9 years ago

That sounds like a good solution to me :). The flow I'm using is similar to the second option - the difference being that instead of showing the user a login screen the client automatically gets new credentials from our webservers before the token expires.

Thanks, I had been wondering what that authKey setting was called. I remember reading about it at some point but I don't think its in the docs atm.

jondubois commented 9 years ago

@hedgepigdaniel Ok, this feature has been implemented in v2.2.30.

You can now specify autoProcessSubscriptions: false in the options object passed to the initial var socket = socketCluster.connect(options) call. It is true by default though.

Given that the auto-resubscribe feature can be switched on/off, I ended up using your solution after all (since backwards compatibility is no longer an issue). I think that somewhere inside the 'connect' event handler is the best place to initiate the resubscription so providing it as a second argument encourages this.

so:

socket.on('connect', function (status, processSubscriptions) {
  // ... Do some stuff
  // You can call the following function asynchronously whenever it's convenient
  processSubscriptions();
});
hedgepigdaniel commented 9 years ago

Thanks Jon, works a treat!