HTTP Authorization Header in EventSource (Server Sent Events) - javascript

I need to set an Authorization header to an HTML5 EventSource. As Server Sent Events seems to be disused since Websockets appeared, I cannot find any useful documentation. The approach I have already found is to pass the authorization data within the url... but I don't like this method.
I am using AngularJS and set interceptors on $httpProvider, but the EventSource is not intercepted by AngularJS, so I cannot add any header.

I realize your post was over a year ago, but I found myself in the same boat with now good answers. I'm hoping this may help someone, or at least give them some ideas...
Cookies seem easy enough, but what happens if someone is blocking cookies? I would have to prompt them to enable cookies to use the site. At that point they start to wonder if they can trust the site since they disabled cookies for 'security reasons'. All the while, I want cookies enabled for security reasons!
Using AJAX, one can easily POST authentication data over SSL, but that's just not possible with SSE. I've seen many posts where people then say, "just use the querystring", but I don't want to compromise a customer's security by sending the auth data in plain text (example.com/stream?sessionID=idvalue) which someone could snoop.
After racking my brain for a couple hours I realized that I CAN accomplish the the overall goal without compromising the customer's auth data. Just to clarify, I haven't discovered some way to POST when establishing an EventSource connection, but it does allow the browser to securely pass an authentication token with the EventSource each time it reconnects. They key is to get the desired sessionID/token into the lastEventID.
The user can authenticate as usual with a username/password (or by AJAX POSTing a token you keep in localstorage). The AJAX auth process will pass back a JSON object with a short-lived-token (expires in 60 seconds, or when used) which would be saved in your desired backend (eg: mySQL) along with a longer-lasting token. At this point you initiate your SSE connection like:
qString = "?slt=" + "value-that-expires-within-seconds";
streamURL = "http://example.com/stream.php";
var streamSource = new EventSource(streamURL + qString);
streamSource.addEventListener('auth',function(e) {
var authStatus = JSON.parse(e.data);
if (authStatus.session !== 'valid') {
qString = "";
streamSource.close();
}
})
In the corresponding PHP you would do something like this:
header("Content-Type: text/event-stream\n");
ob_end_flush();
ob_start();
if (isThisShortLivedTokenValid($_GET["slt"])) {
// The short-lived-token is still valid... so we will lookup
// the value of the corresponding longer-lasting token and
// IMMEDIATELY invalidate the short-lived-token in the db.
sendMsg($realToken,'auth','session','valid');
exit;
} else if (isThisRealTokenValid($_SERVER["HTTP_LAST_EVENT_ID"])){
while (1) {
// normal code goes here
// if ($someCondition == 'newDataAvailable') sendMsg($realToken,'chat','msg-id','msg-content');
}
} else {
http_response_code(404); // stop the browser from reconnecting.
exit; //quit the PHP script and don't send anything.
}
function sendMsg($id, $event, $key, $val) {
echo "{" . PHP_EOL;
echo "event: " . $event . PHP_EOL;
echo "id: $id" . PHP_EOL;
echo 'data: {"' . $key . '" : "' . $val . '"}' . PHP_EOL;
echo "}" . PHP_EOL;
echo PHP_EOL;
ob_flush();
flush();
}
function isThisShortLivedTokenValid($sltValue) {
//stuff to connect to DB and determine if the
//value is still valid for authentication
return $dbResult == $sltValue ? TRUE : FALSE;
}
SSE connects with the short-lived-token, PHP validates against the short-lived-token and deletes it from the DB so it will never be able to AUTH again. This is somewhat similar when you get texted a 6-digit code to login to online banking. We use PHP to push the REAL token (that expires much later) which we retrieved from the database as the event ID. It's not really necessary for Javascript to do anything with this event-- the server will end the connection automatically, but you can listen to the event if you want to do more with it.
At this point, the SSE connection has ended since PHP finished the script. However, the browser will automatically reestablish the connection (usually with 3 seconds). This time, it will send the lastEventId... which we set to the token value before we dropped the connection. On the next connection, this value will be used as our token and the app will run as expected. It's not really necessary to drop the connection as long as you start using the real token as the event-ID when you send messages/events. This token value is transmitted completely encrypted over SSL both when the browser receives it, and in every subsequent connection to the server. The value that was transmitted 'in the clear' was expired within seconds from when we receive & used it and it can no longer be used by anyone that discovers it. If someone does attempt to use it they will receive a 404 RESPONSE.
If you already use the event-stream ID for some other purpose, this may not work 'out of the box' unless you concatenate the auth-token and the previously used value, and split it into variables so it's transparent to the rest of the app. Something like:
// when sending data, send both values
$sseID = $token_value . "_" . $previouslyUsedID;
sendMsg($sseID,'chat','msg-id','msg-content');
// when a new connection is established, break apart the values
$manyIDs = explode("_", $_SERVER["HTTP_LAST_EVENT_ID"])
$token_value = $manyIDs[0]
$previouslyUsedID = $manyIDs[1]

EventSource doesn't have an API for sending HTTP headers to server. I was struggling with this problem too when I was building realtime-chat using SSE.
However I think cookies will be sent automatically if your SSE server is the same server as your authentication server.

This polyfill adds Authorization Header support: https://github.com/Yaffle/EventSource/
So you can do:
new EventSource("https://domain/stream", { authorizationHeader: "Bearer ..." });

The window.EventSource doesn't seem to support passing additional headers yet. Good news is there are some other popular implementations of EventSource that support additional headers. Some of them are as follows:
eventsource
event-source-polyfill
const eventSource = new EventSource(resoureUrl, {
headers: {
'Authorization': 'Bearer ' + authorizationToken
}
});
eventSource.onmessage = result => {
const data = JSON.parse(result.data);
console.log('Data: ', data);
};
eventSource.onerror = err => {
console.log('EventSource error: ', err);
};

If you use this fork of the event-source polyfill you will be able to add authorization headers similarly to the way rafaelzlisboa describes:
https://github.com/AlexGalays/EventSource#923b9a0998fcfd7753040e09aa83764b3cc0230d
Ï don't know if you can provide the authentication header as a second argument like in rafaelzlisboa's example, I got it to work by creating a headers object, and putting my authorization header in there, like this:
new EventSource("https://domain/stream", { headers: { Authorization: Bearer.... }});

the other way to pass auth token is through the URL as query param, but you should take security in consideration. Also add support of authorization through query param on the sever side.

I solved this by additional rest before sse call, this rest is ordinary rest will require the same security protocol that we need for SSE call and get and OTP in response.
And send this OTP to see call query param and validate this OTP in Web filter and replace it with authentication header.

I went through quite a few posts in an attempt to figuring out if the auth token be sent in the EventSource() call. Although there are polyfill alternatives that allow adding headers : https://github.com/whatwg/html/issues/2177
while others mentioned sending auth token over ssl.
Instead of using polyfill EventSource() or sending the auth token in the query params over ssl, send a eventSource identifier (eventSrcUUID) in the params of EventSource url over ssl as follows :-
On user authentication, eventSrcUUID is generated along with the sseEmitter on the server and place in a sseEmitterMap.
Client retrieves the eventSrcUUID from the response and invokes the EventSource() call with the eventSrcUUID in the param. On the server, the sseEmitterMap is referenced to retrieve the the eventSrc object. The sseEmitter object saved in session data is used to send event notifications to client.

Related

Returning back refresh token for google Oauth 2 using php client

I'm using the JS library https://apis.google.com/js/platform.js to log in my user to pass a token to my backend:
gapi.auth2.getAuthInstance().signIn().then(() => {
var user = gapi.auth2.getAuthInstance().currentUser.get();
var authResp = user.getAuthResponse();
var bodyFormData = new FormData();
bodyFormData.append('google_token', authResp.access_token);
When I receive the access token on my server I try use the php client library to generate the rest of the client:
$google = new google();
$google->setAuthConfig('client_secrets.json');
$google->setAccessType('offline');
$google->createAuthUrl();
$google->getRefreshToken();
$google->setAccessToken($thePOSTedAccessToken);
$service = new Google_Service_MyBusiness($google);
The issue is no matter what I do I cannot get my refresh token from client.auth after I create the $client->createAuthUrl(); . So this either means I'm not doing the client properly in php or something else is wrong. I'm asking this question because I've followed all documentation and looked extensively why I'm not receiving my refresh.
You can't mix things like this. If the authorization code was created using JavaScript you will not be able to get a refresh token as JavaScript does not support refresh tokens.
If you want a refresh token then you will need to authorize the user using a server-side language and not a client-side language.
When you you run the code the first time, the user will be displayed with a consent screen. If they consent to your access then you get a code returned. This is an authorization code.
In the example below you can see how the authorization code is exchanged for an access token and refresh token.
Only after $client->authenticate($_GET['code']); is called will $client->getAccessToken(); and $client->getRefreshToken(); work.
Also once the code has been run once, if you try to authorize the user again you will not get a new refresh token, Google assumes that you saved the refresh token they already sent you. If you didn't save it, you would then need to revoke the user's access either using the revoke command in the library or by going to the user's Google account and revoking the application that way.
Sorry, all I have on hand is a web version; not sure if you're running this command-line or web-based.
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/Oauth2Authentication.php';
// Start a session to persist credentials.
session_start();
// Handle authorization flow from the server.
if (! isset($_GET['code'])) {
$client = buildClient();
$auth_url = $client->createAuthUrl();
header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL));
} else {
$client = buildClient();
$client->authenticate($_GET['code']); // Exchange the authencation code for a refresh token and access token.
// Add access token and refresh token to session.
$_SESSION['access_token'] = $client->getAccessToken();
$_SESSION['refresh_token'] = $client->getRefreshToken();
//Redirect back to main script
$redirect_uri = str_replace("oauth2callback.php",$_SESSION['mainScript'],$client->getRedirectUri());
header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL));
}
?>

"redirect_uri_mismatch" when sending authentication code to GoogleAPI

I am having trouble with the authentication process for the GoogleAPI. In the end I want to be able to read the users steps using the GoogleFit API and then store that value in a database. Currently I'm using restdb.io and executing javascript in codehooks.
The documentation from Google that I am following can be found here, clicking on the HTTP/REST option in the code examples. At the moment I am at step 5: I have gotten the users authentication code and stored it in the database. Now I have to POST the code along with some other parameters and get the access and refresh tokens.
If the POST is successful (from what I understand) I should get back a 200-OK message that the request was valid. Google will then POST a JSON body with the access and refresh token to the redirect_uri that I have specified in my GoogleAPI credentials page and the initial request. At redirect_uri I have to handle the request and save the two values.
The problem is that I receive a redirect_uri_mismatch - Bad Request message from Google as a response when executing the request. I get it at the log.debug("ERROR HERE: " + [...]); in the code below:
async function mainFunction(){
const authCode = THIS_IS_MY_AUTHENTICATION_CODE;
try {
var answer = await postRequestToGoogle(authCode);
//do stuff with response from Google
} catch (error) {
//do stuff
}
}
async function postRequestToGoogle(authCode){
//body for the request
const params = "code=" + authCode + "&" +
"client_id=THIS_IS_MY_CLIENT_ID" + "&" +
"client_secret=THIS_IS_MY_CLIENT_SECRET" + "&" +
"redirect_uri=THIS_IS_MY_REDIRECT_URI" + "&" +
"grant_type=authorization_code";
try{
const result = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params})
.then(res => {
log.debug("ERROR HERE: " + JSON.stringify(res.json()));
return res.json();
})
//return JSON back main function
return result;
}catch(error){
//do stuff
}
}
I looked up the error message and tried some things:
Copy and pasted multiple different Authorized redirect URI from the GoogleAPI credentials page into the code to make sure that there is no problem with
http/https
www/no www
trailing slashes
typos or capitalization
Waited for changes to be processed by Google (read that it can more than 30min)
Changed all the other parameters to see if the redirect_uri is actually the problem
If code is changed the message is invalid_grant - Bad Request
If client_id is changed the message is invalid_client - The OAuth client was not found
If client_secret is changed the message is invalid_client - Unauthorized
If the grant_type is changed the message is unsupported_grant_type - Invalid grant_type
That's why I think the issue is the redirect_uri, but it's unclear to me how since I copy&pasted it. Something that came to mind was that maybe the value of redirect_uri gets changed when it's read by Google? Or maybe when the request is being put together? Do some characters have to be replaced?
I tried to analyze the request with Wireshark but didn't think about the fact that it's HTTPS so I would have I would have to decrypt it.. Is that something I should look into?
Thank you for taking the time to read all of this! If you have any advice please let me know :)
Update 16.11.20:
I have created a new OAuth 2.0 Client ID and used the new id/secret in my request. The resulting message the same as before. I will wait and try again tomorrow to see if maybe Google needs some more time. Then I'll try to delete all current IDs and start with a fresh GoogleAPI project.
Update 19.11.20:
Creating a new OAuth 2.0 Client ID did not resolve my problem, neither did creating a whole new GoogleAPI project and adding those credentials into the request. I am in contact with the developers of restdb.io and have asked them to add the Google Auth Library: Node.js Client to the list of supported Node.js packages. Hopefully that will help, I will give it a try as soon as it can be used :)
Update 02.12.20:
No progress so far, but I'm optimistic that the developers will add the package soon. I will post a final update as soon as I am done with this project.

Javascript request to cross orgin PHP application

I have a PHP application that I want to run through XMLHttpRequests on my front end part, this works fine as it's pratically the same as "normal". This works fine.
I get really confused when I start trying to do secure cross origin requests. For instance, some requests I want to only permit if a user is logged in. But, when testing if a session is still there, it doesn't exist anymore. I just don't want random people tapping into someones profile, for instance. Or gain any sort of data I don't want random people to see.
What I want
Requests through this model shown below, but make them secure. Because some data I want for protect to logged-in users only. How do you do that? I can't wrap my mind around that.
Basically, I now have the issue that I can't check on the PHP end if a user has a active session, as PHP sees it as a totally new thing. Essentially, how do you do it like web broswers do it? It's probably just really stupid, but I can't wrap my mind around it.
What I've tried
I've tried requesting where one sets the $_SESSION, then requesting it where it returns $_SESSION, but it return nothing. This means I can't check if the request comes from a loggedin user.
You can use JSON Web Tokens for communicating securely across devices.
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA.
Source
You basically send a token with each request to sources you want to secure behind a login. Then server side you veryfy that token. Upon login you generate that token and include it in your response. In your frontend you can store the token in local storage on the client machine. For example jQuery has this plugin for local storage.
Here is a super basic example of what you need to do in PHP.
Do your login using javascript/jQuery. Send username/password to login.php for example and authenticate the user like you would normally do.
login.php
use \Firebase\JWT\JWT;
$salt = 'some_salt_string';
$algo = 'HS256'; // allowed ones
$params = array(
"iss" => 'http://example.com', // your own identification
"aud" => ['user_id'=>4324], // client identification
"iat" => time(), // start validity
"exp" => time()+(3600*24) // end validity
);
$token = JWT::encode($params, $salt, $algo);
// example data
$data = [
'example_1'=>'value 1',
'example_2'=>'value 2'
];
$response = array_merge($data,['token'=>$token]);
header('Content-Type: application/json');
echo json_encode($response);
auth.php
use \Firebase\JWT\JWT;
/**
* http://stackoverflow.com/questions/40582161/how-to-properly-use-bearer-tokens
* Get header Authorization
* */
function getAuthorizationHeader(){
$headers = null;
if (isset($_SERVER['Authorization'])) {
$headers = trim($_SERVER["Authorization"]);
}
else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI
$headers = trim($_SERVER["HTTP_AUTHORIZATION"]);
} elseif (function_exists('apache_request_headers')) {
$requestHeaders = apache_request_headers();
// Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization)
$requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
//print_r($requestHeaders);
if (isset($requestHeaders['Authorization'])) {
$headers = trim($requestHeaders['Authorization']);
}
}
return $headers;
}
/**
* http://stackoverflow.com/questions/40582161/how-to-properly-use-bearer-tokens
* get access token from header
* */
function getBearerToken() {
$headers = $this->getAuthorizationHeader();
// HEADER: Get the access token from the header
if (!empty($headers)) {
if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
return $matches[1];
}
}
return null;
}
$token = getBearerToken();
$salt = 'some_salt_string';
$algo = 'HS256';
$decoded_token = JWT::decode($token, $salt, $algo); // returns object
// you can access the audience properties to verify the user id against a requested resource
$user_id = $decoded_token->aud->user_id;
// finally check user id and either deny or allow access
javascript
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-storage-api/1.9.4/jquery.storageapi.min.js"></script>
<script>
var storage=$.localStorage;
function login() {
$.ajax({
url: 'https://example.com/api/login',
type: 'POST',
data: {
username:'Username',
password:'Password'
},
success: function (data) {
storage.set('auth_token',data.token); // store returned token to local storage using the jQuery plugin"value"
},
error: function () {
alert('Error occured');
},
});
}
function testApi() {
$.ajax({
url: 'https://example.com/api/test',
type: 'GET',
beforeSend: function (xhr) {
xhr.setRequestHeader('Authorization', 'bearer '+storage.get('auth_token')); // append the token. Mind the space.
},
data: {
query:'value'
},
success: function () {
},
error: function () {
},
});
}
</script>
Include the auth snippet in your secure pages or better said API endpoints.
CORS or cross origin request are to allow other domains to access your web-services. The feature you are asking is user level access or we can say which is public and which is restricted to user roles. It's not something related to CORS. Secondly session is restricted to one system. Please read this page of w3schools for better understanding of session https://www.w3schools.com/php/php_sessions.asp
You can also maintain an user level variable in your mysql or whatever db you are using on the server side to know what is the user level and then bring it to the correct session and with each request check the session value for the type of user like $_SESSION["user_type"] which you can bring from your db and then destroy the session on logout.

$_SESSION variable empty when accessing from AJAX on different port [duplicate]

This question already has answers here:
jQuery Ajax on Different Port
(6 answers)
Closed 8 years ago.
I have created a site which has a PHP side to it this runs on port 80 (index.php), when I login it sets the session variable with the userid.
I then click a link which takes me to port 8080, this is a JavaScript file which carries out node.js processes (live.html).
From this node.js client file (live.html) I request the userid from a file called getUserSession.php which echos out the userId from the session.
When I run the AJAX request (live.html), it returns that the session userid is empty however if I manually insert the URL to the web browser it echos the user session (http://localhost:8080/getUserSession.php).
I have dumped the array into the response and it tells me that the $_SESSION variable is empty.
I also attempted to returned a plain text string test - this returns with no problem, so it seems to be an issue with accessing the session variable from a different port, and not the actual AJAX call itself.
UPDATE:
This is how I eventually resolved the problem.
I added this to the index.html file:
$.getJSON("`http://localhost:80/getUserSession.php?callback=?`", { username: "lazy"}, function(json){
username = json.message;
});
And added the following to the php file:
<?php
session_start();//start the session
$user2 = (string)$_SESSION['username'];
$callback = $_GET["callback"];
$user = $_GET["username"];
if($user == "lazy") {
$response = array("message" => $user2);
} else {
$response = array("message" => $user2);
}
echo $callback . "(". json_encode($response) . ");";
Still needs tidied up but it works. Thanks for the help
When you make the request to php from the browser, a session is created linked to the browser. When you make the request to php using node.js, a new session is created between php and that node.js request because node.js is not the same client as the web browser.
One workaround would be to pass the session token information to node.js so that node.js can request the php file as the same client, however i'm not sure if that's something you can do with php out-of-the-box. With coldfusion it's as simple as passing the cftoken and cfid if you're using the default settings for session management.

HTTP headers in Websockets client API

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 web platform's WebSocket API.
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.
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
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
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).
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.
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.
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 (,) !
For those still struggling in 2021, Node JS global web sockets class has an additional options field in the constructor. if you go to the implementation of the the WebSockets class, you will find this variable declaration. You can see it accepts three params url, which is required, protocols(optional), which is either a string, an array of strings or null. Then a third param which is options. our interest, an object and (still optional). see ...
declare var WebSocket: {
prototype: WebSocket;
new (
uri: string,
protocols?: string | string[] | null,
options?: {
headers: { [headerName: string]: string };
[optionName: string]: any;
} | null,
): WebSocket;
readonly CLOSED: number;
readonly CLOSING: number;
readonly CONNECTING: number;
readonly OPEN: number;
};
If you are using a Node Js library like react , react-native. here is an example of how you can do it.
const ws = new WebSocket(WEB_SOCKETS_URL, null, {
headers: {
['Set-Cookie']: cookie,
},
});
Notice for the protocols I have passed null. If you are using jwt, you can pass the Authorisation header with Bearer + token
Disclaimer, this might not be supported by all browsers outside the box, from the MDN web docs you can see only two params are documented.
see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#syntax
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...
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
}}}
to all future debugger - until today i.e 15-07-21
Browser also don't support sending customer headers to the server, so any such code
import * as sock from 'websocket'
const headers = {
Authorization: "bearer " + token
};
console.log(headers);
const wsclient = new sock.w3cwebsocket(
'wss://' + 'myserver.com' + '/api/ws',
'',
'',
headers,
null
);
This is not going to work in browser. The reason behind that is browser native Websocket constructor does not accept headers.
You can easily get misguided because w3cwebsocket contractor accepts headers as i have shown above. This works in node.js however.
The recommended way to do this is through URL query parameters
// authorization: Basic abc123
// content-type: application/json
let ws = new WebSocket(
"ws://example.com/service?authorization=basic%20abc123&content-type=application%2Fjson"
);
This is considered a safe best-practice because:
Headers aren't supported by WebSockets
Headers are advised against during the HTTP -> WebSocket upgrade because CORS is not enforced
SSL encrypts query paramaters
Browsers don't cache WebSocket connections the same way they do with URLs
What I have found works best is to send your jwt to the server just like a regular message. Have the server listening for this message and verify at that point. If valid add it to your stored list of connections. Otherwise send back a message saying it was invalid and close the connection. Here is the client side code. For context the backend is a nestjs server using Websockets.
socket.send(
JSON.stringify({
event: 'auth',
data: jwt
})
);
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.
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.
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);

Categories

Resources