91

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.

Alvaro Luis Bustamante
  • 8,157
  • 3
  • 28
  • 42

10 Answers10

36

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]
McFly
  • 705
  • 6
  • 9
  • 4
    It seems that you wouldn't even need to do a significant portion of that -- have the client send a normal POST request to, say /getAuthCodeForEventConnection, with it's authorization credentials. The server sends back a short-lived-token to the POST response, which the client immediately sends in with new EventSource(`${uri}?slt=${sltToken})` The server then sets up it's own association between the user and that particular socket connection. Doesn't seem that you need to either disconnect, or use some odd permutation of the last received id field. – Eric Blade May 13 '19 at 00:34
  • 6
    FYI query strings are secured over https, the reason you don't want to use them is they are often logged in loggers. See https://stackoverflow.com/questions/323200/is-an-https-query-string-secure – thinktt Aug 06 '20 at 21:43
  • I have linked the question and your answer in the WHATWG Issue: https://github.com/whatwg/html/issues/2177 - Hopefully, it will help in the future – Tobias Münch Jan 19 '23 at 11:32
23

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.

srigi
  • 1,682
  • 1
  • 15
  • 30
20

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:

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);
};
Manoj Shrestha
  • 4,246
  • 5
  • 47
  • 67
18

This polyfill adds Authorization Header support: https://github.com/Yaffle/EventSource/

So you can do:

new EventSource("https://domain/stream", { authorizationHeader: "Bearer ..." });
rafaelzlisboa
  • 2,638
  • 1
  • 13
  • 7
  • 1
    Can you please provide a snippet of angular working with this? I'm not achieving it :l – pihh-rocks May 26 '16 at 23:47
  • 1
    This polyfill does not support user-defined headers. Did you want to link to a fork perhaps ? – AlexG Jun 15 '16 at 16:42
  • 5
    It's already not a polyfill because native EventSource doesn't support user-defined headers so you simply get a hidden error/bug when your code will use native EventSource. – maksimr Nov 24 '17 at 13:35
  • 21
    A note on this implementation: Having read through the code, it appears that it wraps XMLHttpRequest with a timer to accomplish the EventSource-like results. Not a criticism, just a point to consider when weighing connection load on your server. – Scott D. Strader Feb 07 '19 at 11:35
5

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.... }});

Jeroen Dragt
  • 76
  • 2
  • 5
5

Since this question was asked a lot has happened in EventSource land.

All major browsers now support fetch streams.

Which means that you can now implement an EventSource on top of those streams.
This seems to be the main reason why headers where never added to the EventSource spec.

An example of such a fetch based implementation of EventSource is https://github.com/Azure/fetch-event-source (npm install @microsoft/fetch-event-source).

Steven
  • 2,050
  • 23
  • 20
4

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.

Sergey Leyko
  • 369
  • 6
  • 14
  • SSL/TLS can be used in that case, to ensure that token is safe and secure. Otherwise IMO this idea is most practical. – crollywood Apr 05 '17 at 15:21
  • 6
    @crollywood token is not "safe and secure" because it probably will be saved along the request within the webserver logs (or in one of the possibly many proxies from client to server). – Paolo May 08 '21 at 21:37
2

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.

Naveen Nisad
  • 91
  • 1
  • 6
  • I have also taught about this approach, but it does not conform well, when multiple backends are used with load balancing. But it is valid option in many cases. – Lubo Jan 18 '23 at 17:23
0

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.

alevi.dcosta
  • 143
  • 2
  • 12
0

As others pointed out, the native EventSource implementations of current popular browsers do not support the Authorization header as of today and they'll likely never do. Moreover EventSource is not the Angular HTTPClient, but it's a built-in facility, so, even assuming it supported any HTTP headers, Angular interceptor cannot work with it.

The more modern Fetch API provides streams, which are a more general facility than the old SSE and do not suffer from the XMLHttpRequest limitations either.

If you need to stick to SSE (server side), the Fetch Event Source package is your best bet as of today, because it is based on the Fetch API, which is more up with the times than the original EventSource API and the Fetch API is what Chrome developers decided to mantain in the latest years.

Fetch Event Source does not wrap XMLHttpRequest, but it uses streams from the Fetch API instead, so it doesn't come with well known XMLHttpRequest performance tradeoffs.

It lets you add any HTTP headers you need, including Authorization headers:

await fetchEventSource('/api/sse', {
    headers: {
        'Authorization': 'Bearer my_token', // and/or any other headers you need
    },
    onmessage(ev) {
        console.log(ev.data);
    }
});

Maybe it's possible to write an interceptor for it, but I used it without any, because I just needed it in few places of my code.

Lucio Crusca
  • 1,277
  • 3
  • 15
  • 41
  • The semantics of EventSource is completely different from XMLHttpRequest. Under the hood they work completely differently so while wrappers around both may have similar call signatures, they fundamentally work differently and have different performance characteristics. Someone looking to implement SSE is doing it for a reason and is not looking to use XMLHttpRequest as a solution – Daniel Kats May 08 '23 at 23:30
  • Please note that the EventSource wrapper I posted does NOT wrap XMLHttpRequest, but it uses streams from the Fetch API and, as such, it does not suffer from well know performance problems of XMLHttpRequest wrappers. I've updated my answer accordingly. – Lucio Crusca May 09 '23 at 06:53