
Authenticating JavaScript WebSockets
When I was the naive new guy at TalkJS, someone suggested that I could tackle renewable authentication for our WebSockets. And after only a couple of weeks, it worked great on my machine™.
...but if your token expired while you were in a tunnel then it would break forever.
The Problem
When you enter the tunnel, you disconnect from the internet and your WebSocket connection closes. We start trying to reconnect, but after you leave the tunnel, the attempts to reconnect still fail, because now your token expired.
That's easy, we'll just check for the 401 status code. Right?

IETF RFC 6455
The general WebSocket specification is happy to tell us about that 401 status code:
If the connection could not be opened, either because a direct connection failed or because any proxy used returned an error, then the client MUST Fail the WebSocket Connection and abort the connection attempt. Section 4.1
Certain algorithms and specifications require an endpoint to Fail the WebSocket Connection. To do so, the client MUST Close the WebSocket Connection, and MAY report the problem to the user (which would be especially useful for developers) in an appropriate manner. Section 7.1.7
WHATWG WebSockets Standard
But when it comes to the browser specification, they're having none of it:
User agents must not convey any failure information to scripts... [because it] would allow a script to probe the user’s local network in preparation for an attack. Section 4
At its core, this is because WebSocket connections don't support CORS. The initial WebSocket
connection is treated like a no-cors
request, meaning you cannot receive any information
about the server's response.
Impact
The end result is that if a WebSocket connection fails, you just get told that it failed. This works fine, until you want to try and differentiate between "I am still in a tunnel" and "My auth expired".
The Solution
We can't see the failure reason, so let's make sure there's only one option: network loss. That means that we need to remove any authentication checks from the initial connection. We need in-band authentication.
Rather than checking auth tokens during the initial connection, we accept WebSocket connections from anyone. However, after connecting, you are left with an extremely limited "unauthenticated" session.
Basic Client
// You will need to adjust this based on your WS message schema
// Assuming "AUTH <token>" to authenticate, server responds "AUTH OK/ERROR"
// Assuming server sends "AUTH EXPIRED" when your token expires
let tokenPromise: Promise<string> = fetchToken();
async function authenticate() {
const token = await tokenPromise;
ws.send("AUTH " + token);
}
ws.onOpen = () => authenticate();
ws.onMessage = (event) => {
if (event.data === "AUTH EXPIRED" || event.data === "AUTH ERROR") {
tokenPromise = fetchToken();
authenticate();
}
};
To authenticate the session, the client sends a WebSocket message containing their auth token. If it's valid, they become authenticated, otherwise they get a usable error message so they can refresh their token and try again.
Rather than terminating the connection when your token expires, your connection simply reverts to the unauthenticated state and tells the client. That way, if the WebSocket does close, the client knows it must be a network issue.
Other Tips
Quick list of other things to think about
- Make sure the client is still authenticated before you send them data.
- Clients should reauthenticate before their token expires, so they're always authenticated.
- The client's system time is probably wrong, don't trust it when checking token expiry. The
server should return
validFor
after you authenticate. - You can still pass an auth token in the initial connection, but you should allow the connection and then close the WebSocket with a 3000 status code if it's invalid. This is visible to the client.
Conclusion
Don't try to check auth tokens during the initial WebSocket connection. Do it once the WebSocket is connected, instead. It's much more robust, avoids nasty edge cases, and keeps your users happy.