california app design company

Token-based Header Authentication for WebSockets behind Node.js

July 24, 2018

WebSockets in Javascript

The current state of the WebSockets API for Javascript makes me sad sometimes. The RFC6455 spec that defines WebSockets definitely allows for passing back token-based authentication through the request header. However, the Javascript WebSocket interface simply doesn't allow it, forcing devs to use URL params to send authentication details through to the server. With SSL encryption, this theoretically isn't unsafe (since the URL is encrypted along with the rest of the request), but there are very many concerning cases in which URL params just aren't secure at all. Ideally, secrets like API keys or authentication Tokens would be sent though the request header or even the request body.

Our WebSocket App

At Yeti, I recently came across this problem when trying to set up WebSockets for a recent applicationA kiosk in all its glory

The application was an interactive display to help people see current events and office locations in San Francisco City Hall. To create it, we made a React project that runs in Chrome.

As far as the rest of the tech stack, it was fairly simple. We served this React project on top of an Express/Node.JS server. We also had the server proxy an API route to a Django-powered REST API, which came with its own admin dashboard.

The desired functionality was for an admin user in the built-in Django admin dashboard to have an attractive button that when pressed would automatically reset all of the kiosks.

We opted to use WebSockets, specifically our publisher-subscriber architecture Python Server, and since we wanted a base-level amount of security, we also thought it would be a good idea to add some token-based authentication to our WebSockets connection. Enter the problem with how to send up our authentication token.

It turned out the Javascript WebSocket API doesn't support sending anything through the headers, even though I've used many projects in other languages that enabled you to do just that. In the short term, we opted to send them through the url params and initialize our WebSocket clients like so:

/* src/app.js */
// Initialize
const ws = new WebSocket("wss://somedomain.com?token=<token>");
...

This was passable, but we could do better.

In our server code, we're already proxying calls to /api to another REST endpoint (which was written in Django/Python).

/* server.js */
// Dependencies
const express = require('express');
const app = express();
const port = process.env.PORT;
const expressProxy = require('express-http-proxy');

// Set up API proxy and serve though Express
// `process.env.API_URL` is pointing to the Django Server
const apiProxy = expressProxy(process.env.API_URL, { 
  proxyReqPathResolver: function (req) {
    return `/api${req.path}`;
  }
});
app.use('/api', apiProxy);

// Serve our react's index.html file in the home route
app.get('/', function (req, res) {
  res.sendFile(path.join(__dirname, 'build', 'index.html'));
});

// Start the server
const server = app.listen(port);

Projects do this proxying for several reasons. One is to enable the application to consume API products from outside the app without exposing the actual URL of the service. Another is to remove the API-related secrets from being hardcoded in the front-end codebase to a context that can access them through environment variables, or in other words, the backend. What we figured was that by implementing the same proxying, we could re-define our WebSocket request in a bunch of different ways including adding request header data.

Luckily, this endeavor out to be much easier than expected thanks to a really neat open-source project called http-proxy-middleware that supports WebSockets and Express integration out of the box. Below is all we had to add with the WebSocket proxying code:

/* server.js */
// Dependencies
const express = require('express');
const app = express();
const port = process.env.PORT;
...

const proxy = require('http-proxy-middleware');

// Set up WS Proxy that listens for WS traffic on root route
const wsProxy = proxy('/', {
  'wss://mywebsocketserver.biz', // Where the WS stream goes
  changeOrigin: true,
  ws: true,
  headers: { token: process.ENV.WS_TOKEN } // Token added here
  secure: true // Needed for websocket resources served with 'wss://'
});
app.use(wsProxy)
... 

// Start the server
const server = app.listen(port);

// Handler for the HTTP -> WS upgrade
server.on('upgrade', wsProxy.upgrade);

Caveat

In the spirit of due diligence, while writing this blog article I came up against a big issue with security that should be disclosed. If the above recipe is followed to the letter, then your websocket server is protected on the surface with Token Authentication. But what about the following scenario?:

schematic of token-based websocket

Let's say I have a webpage called mysite.com  that has Javascript calling our proxy at  ws://mysite.com . The proxy appends our secret token to the header and sends it off to the actual websocket server at wsserver.net.

This is all well and good, but what we haven't covered is what if a hacker/fellow developer at freakydeeks.biz catches wind of the proxy path (ws://mysite.com )? What's stoping them from accessing it with the Javascript on their page, and piggybacking the token-based authentication directly to the WebSocket resource at wsserver.net? Simply put, nothing unless you have some form of validation in the server-side code of your WebSocket resource. Checking the origin header (which indicates what domain the browser is currently on when the WebSocket request was made) is a vital thing to include in the connection lifecycle of your WebSocket, because it's set directly by the browser and can't easily be spoofed as far as a quick confirmation-bias-driven Google Search query can attest. Since WebSockets as a protocol are designed to work cross-origin out of the box, this logic usually needs to be manually included.

Conclusion

Using Node.JS to proxy requests to mutate them under the hood can be beneficial. In cases like these, it can also make your product more secure. Unfortunately, this WebSockets API is also available for use in frontend contexts that aren't served from traditional web servers namely React Native. In that case, you'll likely need to do something a bit more involved.

is a Developer at Yeti. Dean is a developer at Yeti, and more importantly the owner of Yeti's office dog, Ben (benjipoops.com). His interests include baking bread, long distance running, and two other activities that cancel each other out. When he's not developing apps, he's drawing diagrams on his desk and drinking tea.

blog comments powered by Disqus
Token-based Header Authentication for WebSockets behind Node.js https://s3-us-west-1.amazonaws.com/yeti-site-media/uploads/blog/.thumbnails/engineer.jpg/engineer-360x0.jpg
Yeti (415) 766-4198 https://s3-us-west-1.amazonaws.com/yeti-site-static/img/yeti-head-blue.png