This post was most recently updated on April 29th, 2022.
5 min read.This article explains how to easily authenticate your WebSocket connections using .NET Core and vanilla JavaScript. The same concept probably applies to all sorts of front-end libraries, although some of them might offer some syntactic sugar on top of it. But it’s simple, and keeping your implementation simple is generally speaking a good idea.
So – this one came up when developing a simple API that’d expose a WebSocket endpoint for seamless notifications to the Web UI. As everything else in the system required authentication, it made sense to require it for WebSockets connection as well. WebSocket security is part of the picture after all.
But first, let’s take a step back and take a quick look at what we’re actually dealing with, shall we?
Background
WebSockets. They’re all the rage, and for a very good reason; WebSockets are a great solution for a very typical web development problem: How to deliver dynamic updates to a view without implementing some sort of finicky polling methods in JavaScript?
After successfully established, they make it possible for the server to force-feed your display layer (running in the browser) constant updates – such as live tweets about someone’s awkward date at a sushi place (like seen on Twitter), all of the pictures of your drunk uncle causing a scene at that one distant cousin’s, whose name you’ve forgotten, wedding (looking at you, Facebook) or a live comment feed for your favorite Mukbang -stream (with the death of Mixer, I suppose you’d go to Twitch for this).
Or maybe you’ll just need to push notifications to your front-end in that one corporate portal you’re developing. Be that as it may, I’m not judging. But it turns out this is not quite as easy as one would hope, at least not in a corporate context.
Problem
It turns out the out-of-the-box functionality for WebSockets doesn’t contain any easy method for authenticating the user establishing the connection. But that’s kind of a requirement, at least in my corporate use case.
I went through a few sources that describe your options. One website claimed “Origins”-header is always passed along in the request – which would be enough to make sure the calls are coming from a legitimate source (even if we wouldn’t get the claims), but that seemed to be untrue. Some claim that you’re stuck with simply having CORS configured, and that’s the level of security you’ll get. Some explain you need to establish the connection first, and then pass credentials down the pipe using webSocket.send(). Some add basic authentication – credentials – to the request (which is both unsafe and not officially supported). Some simply pass the token in the URL they’re requesting (and this one was the second-best option I found!) And some find another way to pass the token to the API.
My solution is essentially the latter, applied.
So, what do you do?
Solution
There are a couple of options – but after a quick review, I ended up going with the one I deemed the most simple: misuse of the “sub-protocol” -parameter of the webSocket.open() -call.
Wait – what’s a sub-protocol?
webSocket.Open comes with 2 parameters:
- URL (required)
- Typically a ws:// or wss// -endpoint that accepts your WebSocket calls.
- protocols (optional)
- string or an array of strings
- can be grabbed server-side
No other data can be relayed in the initial call. But “protocols” seem pretty flexible…
Can you already see where I’m going with this? :)
We can (misuse) the sub-protocol (“protocols”) -part to relay arbitrary data to our API. We COULD also just grab whatever’s sent using webSocket.send(“…”), but that’s one extra step – and you’re in fact establishing the connection without requiring authentication, and would need extra state management for your WebSockets. That’s a couple of extra things that could go wrong!
So, in short, let’s adopt the most thoughtful and least harmful workaround available – see below for steps:
Time needed: 25 minutes
How to easily authenticate
- (Prerequisite): Implement WebSockets for your project
I trust you’ve taken care of this already since you’re at this point.
This article explains the process a little bit (although it was inspired by an issue I ran into):
https://www.koskila.net/httpcontext-websockets-iswebsocketrequest-always-null-in-your-net-core-code/ - Establish the WebSocket call with a token as a sub-protocol
This is pretty simple – in plain JavaScript, it’s one simple line:
let webSocket = new WebSocket('wss://localhost/api/ws','your-token-here');
That’s it. That’s all we need for the front-end for now. The second parameter is meant for the “sub-protocol” but you can use it for whatever you want. - Grab the sub-protocol server-side
This “protocol” (or the first of the string array, rather) is actually the token – you can access it somewhat like this:
string protocol = HttpContext.WebSockets.WebSocketRequestedProtocols[0];
Now we have the token! So what will we do with it? - Validate the token you’re supplying
Okay, so this part will require a bit of C#. See for a sample below:
var openIdConfigurationEndpoint = $"{authorityEndpoint}/.well-known/openid-configuration";
IConfigurationManager configurationManager = new ConfigurationManager(openIdConfigurationEndpoint, new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidIssuer = authorityEndpoint,
ValidAudiences = new[] { "your-api" },
IssuerSigningKeys = openIdConfig.SigningKeys
};
SecurityToken validatedToken;
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
// The below will throw if using an expired or invalid token
var user = handler.ValidateToken(protocol, validationParameters, out validatedToken);
If you need to take some additional authorization steps (like verifying some claims), you can use the user object after calling handler.ValidateToken(). - Remember to accept the WebSocket connection WITH the additional sub-protocol!
This one bit me in the butt and left me confused for a while. The handshake would always fail in the browser after my backend had accepted the connection.
It’s a bit weird, but you need to do something like this in your code:using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(protocol);
AcceptWebSocketAsync() has the protocol as an optional parameter – you need to pass your token there. - The connection is now open! Use it!
If you wish, you can check out this article on how to easily test the WebSocket connection (it doesn’t describe supplying the token, though).
Okay – that was actually pretty simple! It’s trivial to pass the token from the front-end, it’s secured over TLS (WSS instead of WS) and the server-side authentication is simple and performant – enough for our case!
Obviously, if you’re using SignalR (it’s built on top of WebSockets but just a bit more refined) you can also just slap an [Authorize] attribute on your methods, and that just works.
All of the usual caveats apply. If you find a better solution, let me know in the comments section. Would love to know about it, because this one seems like another thoughtful workaround to me. 😉
Note, too, that my sample simply authenticates the user – doesn’t check for authorization of any kind. That part is more use case-specific, so I’m leaving that part for the reader to figure out (as our solution isn’t widely applicable).
References
- https://sahansera.dev/understanding-websockets-with-aspnetcore-5/
- https://docs.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-5.0
- https://www.linkedin.com/pulse/manually-validate-jwt-token-c-ankit-rana/
- https://stackoverflow.com/questions/7363095/javascript-and-websockets-using-specific-protocol
- All of the ways to make WebSockets more secure that didn’t really help (still a good read):
- “Performing cleanup” – Excel is stuck with an old, conflicted file and will never recover. - November 12, 2024
- How to add multiple app URIs for your Entra app registration? - November 5, 2024
- How to access Environment Secrets with GitHub Actions? - October 29, 2024