259

Looks like it's easy to add custom HTTP headers to your websocket client with any HTTP header client which supports this, but I can't find how to do it with the JSON API.

Yet, it seems that there should be support these headers in the spec.

Anyone has a clue on how to achieve it?

var ws = new WebSocket("ws://example.com/service");

Specifically, I need to be able to send an HTTP Authorization header.

MidnightLightning
  • 5,964
  • 5
  • 37
  • 66
Julien Genestoux
  • 26,486
  • 19
  • 63
  • 91
  • 17
    I think a good solution is to allow the WebSocket to connect without authorization, but then block and wait on the server to recieve authorization from the webSocket which will transmit authorization information in its onopen event. – Motes Dec 28 '15 at 15:57
  • The suggestion by @Motes seems to be the best fit. It was very easy to make an authorization call from onOpen which allows you to accept/reject the socket based on the authorization response. I originally attempted sending auth token in Sec-WebSocket-Protocol header but that feels like a hack. – BatteryAcid Oct 29 '17 at 03:59
  • @Motes Hi, could you explain the "block and wait on the server" part ? you mean something like don't process any messages till there's a "auth" message ? – Himal Aug 18 '19 at 04:44
  • @Himal, yes the server design must not send data or accept any other data than authorization in the beginning of the connection. – Motes Aug 18 '19 at 10:37
  • @Motes Thanks for the reply. I was bit confused by the blocking part, becasue to my understanding you can't block the initial `connect` request. I'm using Django channels on the back end and I've designed it to accept the connection on `connect` event. it then sets an "is_auth" flag in the `receive` event (if it sees a valid auth message). if the is_auth flag isn't set and it's not an auth message then it closes the connection. – Himal Aug 18 '19 at 14:40
  • I guess by block I was implying reject any other traffic, or that is as to say do the auth yourself and reject any failed auth like you say. – Motes Aug 18 '19 at 21:14

11 Answers11

262

Updated 2x

Short answer: No, only the path and protocol field can be specified.

Longer answer:

There is no method in the JavaScript WebSockets API for specifying additional headers for the client/browser to send. The HTTP path ("GET /xyz") and protocol header ("Sec-WebSocket-Protocol") can be specified in the WebSocket constructor.

The Sec-WebSocket-Protocol header (which is sometimes extended to be used in websocket specific authentication) is generated from the optional second argument to the WebSocket constructor:

var ws = new WebSocket("ws://example.com/path", "protocol");
var ws = new WebSocket("ws://example.com/path", ["protocol1", "protocol2"]);

The above results in the following headers:

Sec-WebSocket-Protocol: protocol

and

Sec-WebSocket-Protocol: protocol1, protocol2

A common pattern for achieving WebSocket authentication/authorization is to implement a ticketing system where the page hosting the WebSocket client requests a ticket from the server and then passes this ticket during WebSocket connection setup either in the URL/query string, in the protocol field, or required as the first message after the connection is established. The server then only allows the connection to continue if the ticket is valid (exists, has not been already used, client IP encoded in ticket matches, timestamp in ticket is recent, etc). Here is a summary of WebSocket security information: https://devcenter.heroku.com/articles/websocket-security

Basic authentication was formerly an option but this has been deprecated and modern browsers don't send the header even if it is specified.

Basic Auth Info (Deprecated - No longer functional):

NOTE: the following information is no longer accurate in any modern browsers.

The Authorization header is generated from the username and password (or just username) field of the WebSocket URI:

var ws = new WebSocket("ws://username:password@example.com")

The above results in the following header with the string "username:password" base64 encoded:

Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

I have tested basic auth in Chrome 55 and Firefox 50 and verified that the basic auth info is indeed negotiated with the server (this may not work in Safari).

Thanks to Dmitry Frank's for the basic auth answer

nornagon
  • 14,011
  • 16
  • 68
  • 84
kanaka
  • 63,553
  • 21
  • 138
  • 135
  • 49
    I've come across the same problem. Too bad that these standards are so poorly integrated. You'd expect that they look at the XHR API to find requirements (since WebSockets and XHR are related) for the WebSockets API, but it seems they are just developing the API on an island by itself. – eleotlecram May 01 '12 at 14:26
  • 4
    @eleotlecram, join the HyBi working group and propose it. The group is open to the public and there is ongoing work for subsequent versions of the protocol. – kanaka May 01 '12 at 18:59
  • Would it be a bad idea to use the Protocol header values to send (for example) an authorisation hash? Reading here - http://stackoverflow.com/questions/7363095/javascript-and-websockets-using-specific-protocol - it doesn't feel like a bad idea, but I do wonder what the implications are. – Charlie Jun 04 '14 at 16:20
  • 5
    @Charlie: if you fully control the server, that's one option. The more common approach to is generate a ticket/token from your normal HTTP server and then have the client send the ticket/token (either as a query string in the websocket path or as the first websocket message). The websocket server then validates that the ticket/token is valid (hasn't expired, hasn't already been used, coming from same IP as when created, etc). Also, I believe most websockets clients support basic auth (may not be enough for you though). More info: https://devcenter.heroku.com/articles/websocket-security – kanaka Jun 04 '14 at 17:57
  • @eleotiecram According to rfc6455: WebSocket Protocol is designed to supersede existing bidirectional communication technologies that use HTTP as a transportlayer to benefit from existing infrastructure (proxies, filtering,authentication). [...........] The WebSocket Protocol attempts to address the goals of existing bidirectional HTTP technologies in the context of the existing HTTP infrastructure; as such, it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries, even if this implies some complexity specific to the current environment. –  Sep 14 '16 at 18:47
  • 3
    I guess its by design. I am under the impression that the implementation is intentionally borrowing from HTTP but keep them separated as much as possible by design. The text in the specificatio continues: "However, the design does not limit WebSocket to HTTP, and future implementations could use a simpler handshake over a dedicated port without reinventing the entire protocol. This last point is important because the traffic patterns of interactive messaging do not closely match standard HTTP traffic and can induce unusual loads on some components." –  Sep 14 '16 at 18:54
  • 2016 and this is still the case :( – shabunc Oct 07 '16 at 15:04
  • If we cannot add a custom header, can we at least modify a non custom header, for instance, 'Cookie'? – user3631341 Dec 09 '16 at 07:07
  • There is a way to set Basic Authorization HTTP header, see my answer. – Dmitry Frank Jan 07 '17 at 13:36
  • 4
    Unfortunately this doesn't seem to work in Edge. Thanks, MS :/ – sibbl Feb 21 '17 at 11:07
  • `ws://username:password@example.com` this pattern doesn't work in Safari, but works in chrome, ff – Sasi Varunan Jul 25 '17 at 13:58
  • [how do i set a "connection" header?](https://stackoverflow.com/questions/53798531/error-during-websocket-handshake-connection-header-is-missing) thanks – oldboy Dec 16 '18 at 02:35
  • 1
    Does Basic auth still work? I've tested Chrome and Firefox and there is no Authorization header. – Kegan Thorrez Jan 30 '19 at 23:53
  • 1
    Basic auth won't work anymore as the approach is deprecated. https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#Access_using_credentials_in_the_URL – pkgajulapalli Feb 07 '19 at 10:12
  • When I tried to use "Sec-WebSocket-Protocol" in chrome, I got `WebSocket: Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received` error. – pkgajulapalli Feb 07 '19 at 10:13
  • @pkgajulapalli I've updated the answer to show that basic auth is deprecated. The reason you get that error is because the server needs to support the protocol field and answer with the protocol value that was selected. – kanaka Feb 07 '19 at 17:30
  • @kanaka do you have a link to where it says basic auth is deprecated? – felixfbecker Mar 07 '19 at 19:41
  • 1
    @shabunc 2019 and this is still the case :( – andrhamm Jul 19 '19 at 00:44
63

More of an alternate solution, but all modern browsers send the domain cookies along with the connection, so using:

var authToken = 'R3YKZFKBVi';

document.cookie = 'X-Authorization=' + authToken + '; path=/';

var ws = new WebSocket(
    'wss://localhost:9000/wss/'
);

End up with the request connection headers:

Cookie: X-Authorization=R3YKZFKBVi
Tim
  • 1,755
  • 18
  • 17
  • 2
    This does seem like the best way to pass access tokens on connection. At the moment. – cammil Feb 23 '20 at 10:58
  • 5
    what if the WS server URI is different from client URI? – Danish May 07 '20 at 00:11
  • 5
    @Danish Well then that doesn't work, since you cannot set cookies for other domains client side – Tofandel Jul 03 '20 at 20:12
  • 3
    But, you can set up an HTTP service that sets a session cookie on the relevant path, and call that before starting your websocket. Call, say, `https://example.com/login`, and have the response set a cookie on `/wss` then `new WebSocket("wss://example.com/wss")` will start its handshake request with the relevant cookie. Note that the devtools might not show the cookie, but it should still be sent. – Coderer Jul 09 '20 at 12:52
  • 3
    Websocket requests are not subject to the same origin policy. Sending authentication as a cookie will open up your application to hijacking. See https://christian-schneider.net/CrossSiteWebSocketHijacking.html – Declan Shanaghy Oct 19 '20 at 18:43
  • 1
    Christian Schneider (the author of the article above) suggests using CSRF-Tokens to protect initial HTTP handshake, when using Authentication Cookie: `ws = new WebSocket("wss://example.com/wss?csrftoken=")` – Alex Buchatski Mar 30 '21 at 10:31
  • You could also simply check the origin. – iwaduarte May 07 '21 at 19:11
  • @Coderer could explain how would I achieve this pattern? I am trying to integrate with cookies but those different domains will not allow it (localhost development) – iwaduarte May 07 '21 at 22:25
  • The cookie has to be set on the domain that you're connecting to, not the Origin where your script is served from. That's why I suggested having `example.com/login` return a `Set-Cookie: Session=xyz123`. Once `example.com` has a session cookie associated with it, `new WebSocket("wss://example.com/wss/")` will send the `Session` cookie along with the handshake. As Declan points out, though, this can lead to hijacking. If you control the backend, it'd be better to send credentials over the socket connection. – Coderer May 11 '21 at 08:05
36

HTTP Authorization header problem can be addressed with the following:

var ws = new WebSocket("ws://username:password@example.com/service");

Then, a proper Basic Authorization HTTP header will be set with the provided username and password. If you need Basic Authorization, then you're all set.


I want to use Bearer however, and I resorted to the following trick: I connect to the server as follows:

var ws = new WebSocket("ws://my_token@example.com/service");

And when my code at the server side receives Basic Authorization header with non-empty username and empty password, then it interprets the username as a token.

Dmitry Frank
  • 9,453
  • 7
  • 55
  • 101
  • 13
    I am trying the solution suggested by you. But I am not able to see the Authorization header being added to my request. I have tried it using different browsers e.g. Chrome V56, Firefox V51.0 I am running my server on my localhost. so the websocket url is "ws://myusername:mypassword@localhost:8080/mywebsocket". Any idea what might be wrong? Thanks – LearnToLive Mar 11 '17 at 21:15
  • 5
    Is it safe to transfer token through url? – Mergasov May 04 '17 at 18:02
  • 1
    @Mergasov same thing is questionable for Basic Authentication right? – Rasika Perera May 25 '17 at 06:27
  • 1
    Wouldn't the basic authentication headers be hidden if secure connection is used, while the URL would be open to everybody snooping? – rslite Jun 04 '17 at 03:02
  • 2
    Empty/ignored username and non-empty password as token might be better because usernames might get logged. – AndreKR Jun 22 '17 at 01:28
  • 9
    I agree with @LearnToLive - I used this with wss (e.g. `wss://user:password@myhost.com/ws`) and got no `Authorization` header on the server side (using Chrome version 60) – user9645 Aug 04 '17 at 13:59
  • 1
    @rslite In a secure connection, everything including the URL is hidden inside of secure tunnel. – Ajay M Nov 08 '17 at 05:39
  • 6
    I have the same issue as @LearnToLive and @user9645; neither chrome nor firefox are adding the authorization header when the URI is in the `wss://user:pass@host` format. Is this not supported by browsers, or is something going wrong with the handshake? – David Kaczynski Feb 02 '18 at 20:42
  • 1
    same as @DavidKaczynski Anyone know why? Maybe new version drop the support? – Altiano Gerung Mar 22 '18 at 08:24
  • 1
    @user9645, that is incorrect! The URL is not encrypted. Even the same in HTTPS. – bman Apr 10 '18 at 03:21
  • 6
    The use of these urls `http://username:password@example.com` is depreciated. https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication. Maybe thats the reason it does not work with websockets either! – Dachstein Aug 19 '18 at 18:14
25

Sending Authorization header is not possible.

Attaching a token query parameter is an option. However, in some circumstances, it may be undesirable to send your main login token in plain text as a query parameter because it is more opaque than using a header and will end up being logged whoknowswhere. If this raises security concerns for you, an alternative is to use a secondary JWT token just for the web socket stuff.

Create a REST endpoint for generating this JWT, which can of course only be accessed by users authenticated with your primary login token (transmitted via header). The web socket JWT can be configured differently than your login token, e.g. with a shorter timeout, so it's safer to send around as query param of your upgrade request.

Create a separate JwtAuthHandler for the same route you register the SockJS eventbusHandler on. Make sure your auth handler is registered first, so you can check the web socket token against your database (the JWT should be somehow linked to your user in the backend).

  • This was the only secure solution I could come up with for API Gateway websockets. Slack does something similar with their RTM API and they have a 30 second timeout. – andrhamm Jul 19 '19 at 00:42
21

You can not send custom header when you want to establish WebSockets connection using JavaScript WebSockets API. You can use Subprotocols headers by using the second WebSocket class constructor:

var ws = new WebSocket("ws://example.com/service", "soap");

and then you can get the Subprotocols headers using Sec-WebSocket-Protocol key on the server.

There is also a limitation, your Subprotocols headers values can not contain a comma (,) !

wonea
  • 3,987
  • 17
  • 71
  • 134
Saeed Zarinfam
  • 8,782
  • 7
  • 55
  • 66
21

You cannot add headers but, if you just need to pass values to the server at the moment of the connection, you can specify a query string part on the url:

var ws = new WebSocket("ws://example.com/service?key1=value1&key2=value2");

That URL is valid but - of course - you'll need to modify your server code to parse it.

  • 20
    need to be careful with this solution, the query string may be intercepted, logged in proxies etc. so passing sensitive info (users / password / authentication tokens) this way won't be secure enough. – Nir Apr 23 '17 at 13:46
  • 5
    @Nir with WSS the querystring should probably be safe – Sebastien Lorber May 29 '17 at 14:49
  • 9
    ws is plain text. Using ws protocol anything can be intercepted. – Gabriele Carioli May 30 '17 at 15:05
  • 1
    @SebastienLorber it is not safe to use query string it is not being encrypted the same applies to HTTPS, but since "ws://..." protocol is used, it really doesn't matter. – Lu4 Sep 20 '17 at 00:32
  • 8
    @Lu4 the query string is encrypted, but there are a host of other reasons not to add sensitive data as URL query parameters https://stackoverflow.com/questions/499591/are-https-urls-encrypted/499594#499594 & https://blog.httpwatch.com/2009/02/20/how-secure-are-query-strings-over-https/ as refs –  Nov 27 '17 at 19:53
4

In my situation (Azure Time Series Insights wss://)

Using the ReconnectingWebsocket wrapper and was able to achieve adding headers with a simple solution:

socket.onopen = function(e) {
    socket.send(payload);
};

Where payload in this case is:

{
  "headers": {
    "Authorization": "Bearer TOKEN",
    "x-ms-client-request-id": "CLIENT_ID"
}, 
"content": {
  "searchSpan": {
    "from": "UTCDATETIME",
    "to": "UTCDATETIME"
  },
"top": {
  "sort": [
    {
      "input": {"builtInProperty": "$ts"},
      "order": "Asc"
    }], 
"count": 1000
}}}
  • It doesn't look like generic way to pass custom headers as don't see an option in their [API](https://github.com/pladaria/reconnecting-websocket#available-options). Is it a generic way or works only with Azure and some server side configuration? – Naga Kiran Apr 18 '21 at 16:38
3

Totally hacked it like this, thanks to kanaka's answer.

Client:

var ws = new WebSocket(
    'ws://localhost:8080/connect/' + this.state.room.id, 
    store('token') || cookie('token') 
);

Server (using Koa2 in this example, but should be similar wherever):

var url = ctx.websocket.upgradeReq.url; // can use to get url/query params
var authToken = ctx.websocket.upgradeReq.headers['sec-websocket-protocol'];
// Can then decode the auth token and do any session/user stuff...
Ryan Weiss
  • 1,099
  • 1
  • 12
  • 30
  • 4
    Doesn't this just pass your token in the section where your client is supposed to request one or more specific protocols? I can get this working, no problem too, but I decided not to do this and rather do what Motes suggested and block until the auth token is sent on the onOpen(). Overloading the protocol request header seems wrong to me, and as my API is for public consumption, I think it is going to be kinda confusing for consumers of my API. – Jay Jun 27 '17 at 09:57
-1

You can pass the headers as a key-value in the third parameter (options) inside an object. Example with Authorization token. Left the protocol (second parameter) as null


ws = new WebSocket(‘ws://localhost’, null, { headers: { Authorization: token }})

Edit: Seems that this approach only works with nodejs library not with standard browser implementation. Leaving it because it might be useful to some people.

Nodens
  • 318
  • 1
  • 3
  • 10
  • Had my hopes up for a sec. There doesn't appear to be an overload taking a 3rd param in WebSocket ctor. – Levitikon Dec 18 '19 at 14:18
  • Got the idea from wscat code. https://github.com/websockets/wscat/blob/master/bin/wscat line 261 wich uses the ws package. Thought this was an standard usage. – Nodens Jan 16 '20 at 11:29
  • This doesn't work for client side and at the server this answer seems irrelevant. – iwaduarte May 07 '21 at 22:25
-1

My case:

  • I want to connect to a production WS server a www.mycompany.com/api/ws...
  • using real credentials (a session cookie)...
  • from a local page (localhost:8000).

Setting document.cookie = "sessionid=foobar;path=/" won't help as domains don't match.

The solution:

Add 127.0.0.1 wsdev.company.com to /etc/hosts.

This way your browser will use cookies from mycompany.com when connecting to www.mycompany.com/api/ws as you are connecting from a valid subdomain wsdev.company.com.

Max Malysh
  • 21,876
  • 16
  • 91
  • 102
-4

Technically, you will be sending these headers through the connect function before the protocol upgrade phase. This worked for me in a nodejs project:

var WebSocketClient = require('websocket').client;
var ws = new WebSocketClient();
ws.connect(url, '', headers);
George
  • 455
  • 3
  • 10
  • 3
    This is for the websocket client in npm (for node). https://www.npmjs.com/package/websocket Overall this would be exactly what I am looking for, but in the browser. – arnuschky Aug 16 '17 at 22:50
  • 2
    It's downvoted because this headers parameter in on the WebSocket protocol layer, and the question is about HTTP headers. – Toilal Nov 30 '17 at 13:49
  • "`headers` should be either null or an object specifying additional arbitrary HTTP request headers to send along with the request." from [WebSocketClient.md](https://github.com/theturtle32/WebSocket-Node/blob/master/docs/WebSocketClient.md#connectrequesturl-requestedprotocols-origin-headers-requestoptions); therefore, the `headers` here is HTTP layer. – momocow Oct 28 '18 at 07:41
  • Also, anyone who wants to provide custom headers should keep in mind the function signature of the `connect` method, described as `connect(requestUrl, requestedProtocols, [[[origin], headers], requestOptions])`, i.e. the `headers` should be provided along with `requestOptions`, for example, `ws.connect(url, '', headers, null)`. Only the `origin` string can be ignored in this case. – momocow Oct 28 '18 at 07:47