yeti logo icon
Close Icon
contact us
Yeti postage stamp
We'll reply within 24 hours.
Thank you! Your message has been received!
A yeti hand giving a thumb's up
Oops! Something went wrong while submitting the form.

Token-based Header Authentication for WebSockets behind Node.js

By
-
July 24, 2018

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 application

A 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 */// Initializeconst 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 */// Dependenciesconst 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 Serverconst 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 routeapp.get('/', function (req, res) {  res.sendFile(path.join(__dirname, 'build', 'index.html'));});// Start the serverconst 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 */// Dependenciesconst 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 routeconst 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 serverconst server = app.listen(port);// Handler for the HTTP -> WS upgradeserver.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.

You Might also like...

colorful swirlsAn Introduction to Neural Networks

Join James McNamara in this insightful talk as he navigates the intricate world of neural networks, deep learning, and artificial intelligence. From the evolution of architectures like CNNs and RNNs to groundbreaking techniques like word embeddings and transformers, discover the transformative impact of AI in image recognition, natural language processing, and even coding assistance.

A keyboardThe Symbolicon: My Journey to an Ineffective 10-key Keyboard

Join developer Jonny in exploring the Symbolicon, a unique 10-key custom keyboard inspired by the Braille alphabet. Delve into the conceptualization, ideas, and the hands-on process of building this unique keyboard!

Cross-Domain Product Analytics with PostHog

Insightful product analytics can provide a treasure trove of valuable user information. Unfortunately, there are also a large number of roadblocks to obtaining accurate user data. In this article we explore PostHog and how it can help you improve your cross-domain user analytics.

Browse all Blog Articles

Ready for your new product adventure?

Let's Get Started