WebSocket security with Phoenix Channels

We've been working with WebSockets for a while now and we've found the Phoenix Channel implementation to be really effective.

One thing we haven't seen discussed on a huge level is security. Most of the examples generally discuss using an authentication token like so

this._super('wss://our_api.com/data_socket', {
    params: {
        guardian_token: userJwt
    }
});

(We are using Guardian for authentication in the example above).

Logging of Tokens

The problem with the above is that the token gets sent as a query string value when the WebSocket handshake takes place. We are not worried about man-in-the-middle attacks as the connection is made securely by using wss. The problem is that query string values are often stored in log files on servers and that potentially means we have a log file full of authentication tokens that can be reused, which is a security risk.

How to connect securely

Advice from Heroku

There is a good article on WebSocket Security on Heroku, which has a good piece on Authentication / Authourisation. What it suggests is that before doing the WebSocket handshake, a token is requested that contains the user ID, the IP of the client requesting the ticket and a timestamp.

Our approach

We have taken Heroku's advice and tweaked it slighty to fit our approached with JWT tokens.

What we suggest is that developers do the following before connecting to a socket.

  1. Send a request to create a new WebSocket Authorisation token. This should be done using a POST request to your API.

    let data = createBodyWithJWTInIt,
         url = getAPIURL('/authenticate_websocket');
    
       $.post(
         url,
         data
       ).done((response) => {
          // Grab your new token from the response
          let singleTimeUsageJWT = .....;
     });
  2. The created token should be a short lived, one time usage JWT token. By short lived, we mean no more than 10 seconds.

  3. Connect to the Socket using the newly created token

    this._super('wss://our_api.com/data_socket', {
         params: {
             guardian_token: singleTimeUsageJWT
         }
     });
  4. Mark the token as used

Yes, the new token is still sent via the query string, however as it's such a short lived token, and can only be used once, then there are no security concerns there.

Why not just stop using the Query string?

There aren't really a lot of options when it comes to connecting to a WebSocket via a browser. Firstly, the request has to be done via a GET request, which then gets upgraded to a WebSocket. There is no option to do this via a POST request.

Another option with authorisation normally is to pass the token via a header, however, since you cannot customize WebSocket headers from JavaScript this is not possible either.