Hey those of you doing web sockets with
Hey those of you doing web sockets with durable objects, how do you authenticate the request to open the websocket? I guess there is no proper standard way of doing it?
46 Replies
I guess alternatives include at least:
- pass a token as query param (which is not optimal)
- custom header (if client & server support those)
- post a token as the first message into the socket
You can use the
Authorization
header or pass the token in via a query parameter. It's better to perform the auth check before allowing the websocket to create a connection.
It's of no consequence if you pass the token as a query parameter, Authorization
header would be more standard.Query params do have the side effect that they might get logged to access logs a long the way which is not great
If a third-party has access to your logs; you most likely have a larger issue at play. Headers are fine.
You can also use JSON Web Tokens (JWTs) if you wish
https://hono.dev/docs/helpers/jwt
Third parties do not have access to the logs, but as a rule of thumb you sensitive data should not be logged..
If you require harder security you should assume the entire transport layer is bugged and look at JWT to sign the messages.
ChatGPT thinks
"the standard WebSocket API in browsers (i.e., new WebSocket(...)) does not support setting custom headers, including Authorization" dunno if this still holds true, or everyone is using something else than the standard WS API in browsers to be abe to set headers? (regardless if its jwt, session token or something else in the content)
We just use query parameters for the token on websocket, auth headers for HTTP requests to the same endpoint. No one has hacked it yet. Look at JWT, it's probably what you want.
Websocket connection don't allow custom headers, this is true.
Thanks for the input. Curious to hear also what others are doing.
It's either query param in the websocket connection, JWT, or a custom handshake. Can't imagine any other possible options.
not exactly sure what you mean by just "JWT", jwt as a query param or what do you refer to?
jwt is typically delivered in the auth header which we just concluded does not work here, so not sure what you mean by this
Put JWT in the route before the ws endpoint
Or just use authorization header with a simple token
Something like:
Not using hono, and I am interested in what moves in the network, not the framework abstraction.
Okay, so in your application code, you must implement a middleware solution such that you can run code before your websocket connection upgrades.
It's not rocket surgery. The key take-away is performing the auth before websocket client is allowed to connect.
well that I can of course do in the worker, but I try to understand what is the point of the jwt token if it can't be delivered in the authorisation header when opening the socket (edit can -> can't 🙂 )
JWT token is signed and verified
I know jwt tokens very well, but there needs to be something that travels from the browser to the server, that is the point of the whole discussion. And I am now asking how does the jwt get delivered.
I would strongly recommend using
hono
if you aren't familiar with implementing secure connections and security is a concern.
Read up on it
over HTTP via a handshake
the server won't allow the websocket client to connect until auth has happened first
through the use of a middleware
I'm pretty sure that JWT websocket implementations will use the query parameter, to my best knowledge. Read into it.ok so in the end "JWT" it's just the query param solution (but with specific type of token as the param)
I don't think there is another solid way of doing it, like I first said, sending the token in the query parameter is not really of consequence.
You could implement the handshake inside the connection after it's established. I think it's better to not allow extra connections. YMMV. Be sure to use
wss
Probably best to use a short-lived token and pass via query params. Using a handshake inside the connection risks DOS. Let's see what everyone else is doing @1984 Ford Laser might have some insights. I'm curious now.Had a brief flick of the thread, short lived tokens would be a good method for prod. I have used simple URL params for testing and basic usage
99% sure either CF or your browser will try an force secure mode, I accidentally ran into this today with a mistyped URL of
ws://
where it simply errored out somewhere cos of itCookies do get passed in on the WS upgrate HTTP request. So, complete your authentication cycle using HTTP at the Worker level (not DO) and end by setting a cookie with your sessionId or sessionJWT
And use an http-only and secure-only cookie for security reasons
Once the WS connection is made you don't have to auth check the messages. If you want to log someone off, invalidate the token and close the WS connection. They will have to re-authenticate when they try to reconnect
yeah that latter part is under control already, was just not super happy about sending the auth token as query param. That cookie based approach sounds more approriate, thanks 👍
I'm also weary of putting secrets on the query string.
I did similar research into this recently and here's two additional options:
1. Abuse the ws subprotocol. if you pass a second parameter to the client initialization it'll get passed into the
Sec-WebSocket-Protocol
header. so you could do something like new WebSocket('wss://...', ['jwt', token])
and parse the header on the server. For whatever reason this is generally discouraged.
2. The "proper" way is to have the first message be your auth message. I agree that it feels concerning to allow the connection without auth but this seems to be the most official solution. To mitigate security concerns, you can create a base class with a wrapper around the ws functionality that ensures the user authenticated. When a message comes in, ensure the first message is for auth and and disconnect the user if it isn't, then future messages can be passed along to a different method you expose to actually receive messages in the extending class. Then also wrap sending messages to ensure the connection was authenticated so you never accidentally send messages to unauthenticated connections. You could also add brief timer to disconnect the connection if it hasn't sent the auth message yet. It's a lot, and annoying to have to do all that work instead of just checking a header, but is what it is 🤷♂️Using cookies for the websocket auth can introduce more subtle security risks. I'd be wary of using cookies for this and lean more towards a stateless approach with a short-lived token.
Using
Sec-WebSocket-Protocol
is discouraged because it's outside of the intention of what the header does. It could break interoperability or be treated in hard to predict ways by intermediary proxies. Using Sec-WebSocket-Protocol
should work in most cases, if it doesn't work or gets leaked, it might be difficult to figure out why.
Utilizing a handshake inside the websocket connection can risk DOS or other resource based attacks. We prefer to not allow the WS connections to happen until we've already authenticated.Can you be more specific about the security risk of http-only, secure-only cookies? My day job is application security and while a determined developer can shoot themselves in the foot with just about any technology, this is the one I have had the least luck exploiting.
I believe Cross-Site WebSocket Hijacking (CSWH) would be an issue, you'd need to verify origin or use SameSite? Websockets are not restrained by the same-origin policy. Not super difficult to implement.
The other issue is that if you wish to support clients that don't use cookies, you would need to implement an additional authentication mechanism outside of cookies.
Using cookies for websocket auth implies a stateful authentication instead of a stateless. There are trade-offs. I'm under the belief that going query params with stateless short-lived tokens is overall easiest to get right. I would venture to say using short-lived token + cookies as Access token / Refresh token could work well; still same issue if client doesn't support cookies.
As you say, CSWSH is possible when the server doesn't check the origin during the handshake. Cloudflare does, so that's not a risk here, but even if it were, that is not a function of using cookies. You can accomoplish CSWSH with any approach.
Fair enough, but YAGNI
Are you 100% certain that CF validates origin on websocket connections? I think you may need a custom WAF for that.
While I never say 100%, try to create a counter-example.
I don't need to; we aren't using cookies for auth. I don't think CF is going to validate origin on websocket upgrade requests.
I'd be interested to know for sure.
Cookies are not a factor in CSWSH. If it's a problem with cookies, it's a problem with your approach
That's partially true, a bit misleading. I agree in principal.
Like I originally said, introduces subtle issues. I don't believe CF is validating origin on websocket upgrade requests either. All theoretically to me here today. Just doing my best to answer your initial question.
Ahh, I think I may understand the difference in perspective. CSWSH with cookies is more likely if you set the cookie samesite option to 'None'
I think the key take-away is that if you are using a stateful auth for websocket you need to be extra careful to verify origin, and take other precautions related to stateful auth. It's not treated exactly the same as regular HTTP.
So, the full recommendations for setting cookies are like this (if you are using Hono):
That looks correct, so it's
secure
, httpOnly
, and sameSite
I would still probably validate the Origin header itself on any WebSocket upgrade pathsI failed to mention sameSite to @Tero earlier
Good conversation @Larry, I appreciate your input here. I'm no expert on the subject matter. Happy to learn more as we go.
Me too!
You might also want to read this: https://deoxy.dev/blog/stop-using-jwt-for-auth/
deoxy.dev
Stop Using JWT for Authentication: The Stateless Myth
Learn why JWT might not be the best choice for session management and authentication, and why truly stateless authentication is often a misleading concept.
That makes sense. I misspoke before, we are actually using a stateful session here, not a stateless. Session state is inside the Durable Object instance. Not using JWT at all. We do keep pretty strict XSS hygiene. Requirement for non-browser clients still exists.
The "proper" way is to have the first message be your auth message. I agree that it feels concerning to allow the connection without auth but this seems to be the most official solution.I see this being especially problematic for durable objects as they are single threaded. So even if the auth operation for the first request was relatively fast, say 1ms to validate some type of signeded token, the load required to make the DO unresponsive for rest of the clients would not need to be very much. You need to establish a connection and then close it after failing auth so it makes each attempt rather expensive in terms of resource usage. If you compare this to a solution where you have the auth happening in the worker in front of the DO, you know that can scale to hundreds of instances across different data centers if necessary.
Fair. Definitely nicer if you can handle the auth in the worker before reaching out to the DO if that's an option.
As far as that actually being an issue I suppose depends on your DO usage. I imagine there are many scenarios that fall into that category you described that have nothing to do with WS auth. Just using the DO in general would also have those blocking requests so I'd say that's just par for the course. If you're worried about DOS or something I imagine the need for rate limiting applies about equally to a public facing DO and the authentication step.