DEV Community

Kailash Sankar
Kailash Sankar

Posted on

Websockets with React & Express [Part-3]

In the last part we setup a working client and server communicating via websocket connection, we also managed to send individual messages and broadcast messages. This part will focus on adding authentication.

We cannot add headers to websocket requests, so the approach of passing something like Bearer token wouldn't work out. There are a few ways to go around this, I'll pick the easiest one for this tutorial but for a production system you would have to make it more robust.

If you've noticed the WebSocket interface in the client, it accepts a second parameter which can either be a string or an array of strings

WebSocket(url[, protocols])
Enter fullscreen mode Exit fullscreen mode

The values passed in protocols gets added to sec-websocket-protocol header of the upgrade request. It is meant to be used for sending protocol info like "soap" but we can repurpose it to pass our auth token and update our server code to authenticate with it. Another quick way is to pass the token as a URL parameter. Using the protocol header or passing it via url doesn't seem like the best way forward but let's play around with it a bit. We will take about better approaches at the end.

Note that cookies if any are passed along by default. I have a jwt token in my application, it's given to logged in users and I am going to use the same for WebSocket authentication. You can choose an approach depending on the auth system you have.

Update the demo component to pass the auth token via url, let's add a route as well, we might have different routes in future with it's own purpose and logic.

  const ws = useWebSocketLite({
    socketUrl: sockerUrl + `/demo?token=${user.authToken}`,
  });
Enter fullscreen mode Exit fullscreen mode

Now we move to the server, get the token from url and use it for authentication. I already have express-jwt middleware setup for my express server, it can be reused to authenticate our websocket request.

We start by writing a function to get the params from url

// utils.js

// available as part of nodejs
const url = require("url");

// returns the path and params of input url
// the url will be of the format '/demo?token=<token_string>
const getParams = (request) => {
  try {
    const parsed = url.parse(request.url);
    const res = { path: parsed.pathname };
    // split our query params
    parsed.query.split("&").forEach((param) => {
      const [k, v] = param.split("=");
      res[k] = v;
    });
    return res;
  } catch (err) {
    return "na";
  }
};

/* return format
{
  path: '/demo',
  token: '<token_string>'
}
*/
Enter fullscreen mode Exit fullscreen mode

Modify the upgrade function to authenticate token

// setupWebSocket.js

// update the upgrade function to add authentication
  server.on("upgrade", function upgrade(request, socket, head) {
    try {
      // get the parameters from the url
      // path is ignored for now
      const { token } = getParams(request.url);

      // pass the token and authenticate
      if (token) {
        const req = { token };

        // authentication middleware
        authenticateWS(req, {}, (err) => {
          // following the express middleware approach
          if (err) {
            throw err;
          }
          // user information will be available in req object
          // allow upgrade to web socket
          wss.handleUpgrade(request, socket, head, function done(ws) {
            wss.emit("connection", ws, request);
          });
        });
      } else {
        throw "No token found";
      }
    } catch (err) {
      console.log("upgrade exception", err);
      socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
      socket.destroy();
      return;
    }
  });
Enter fullscreen mode Exit fullscreen mode

Define the jwt authentication function,

// tweaked express-jwt

const jwt = require("express-jwt");
const secret = process.env.JWT_SECRET;

// express-jwt looks for the token in header by default
// we pass a getToken function to override that behaviour
// our callback function is called on success/failure(with error)
// on success, the req object would have user info
const authenticateWS = jwt({
  secret: secret,
  getToken: function (req) {
    return req.token;
  },
});
Enter fullscreen mode Exit fullscreen mode

The authentication layer is done and it leads the way for authorization, jwt sets the user object in req and giving us a way to access user.role (provided you are setting it during login).

Next, we look at the right ways of securing our server,

  • the server should use wss:// instead of ws://
  • use cookies if your client is setting it
  • pass the token as part of your first payload, consider this as an additional step to the websocket protocol. Note that you will incur the cost of establishing a connection before being able to authenticate it.

How about we combine all these approaches to come up with something safer without a lot of overhead.

  1. We keep the websocket layer behind our application login
  2. Before a socket request, we generate a random token from server specifically to validate the ws connection
  3. Pass the token along with the ws request as a url param, check for this token in the upgrade header, if it's valid allow the upgrade
  4. Pass the jwt token in the first message payload. Validate & authorise the user, terminate connection if jwt is invalid

We'll get in to the details and benefits of this in the next part.

Recommended reading: Websocket Security, html5 websocket

Top comments (0)