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.
At Yeti, I recently came across this problem when trying to set up WebSockets for a recent application
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);
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?:
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.
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.