HTTP Authorization Header in EventSource (Server Sent Events)

前端 未结 8 1995
梦谈多话
梦谈多话 2021-02-01 01:52

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 a

8条回答
  •  温柔的废话
    2021-02-01 02:12

    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]
    

提交回复
热议问题