Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cookies don't work over WebSocket on Apple devices

Iv'e been setting and retrieving cookies over pre-WebSocket stage to identify a user. I assumed everything would work as over a typical HTTP exchange.

This has worked flawlessly on all browsers I've tested them on, but reports started coming in that on iPhones the sign-ins would not be retained at all, signifying that cookies either were not set or sent back to the server.

// fret not, safety checks removed for brevity

const (
    sessionKeyCookieName string = "session-key"
    webSocketPath        string = "/ws"
)

func serveWs(w http.ResponseWriter, r *http.Request) {
    var sessionKey [sha1.Size]byte
    var u *user
    for _, cookie := range r.Cookies() {
        if cookie.Name != sessionKeyCookieName {
            continue
        }
        slice, err := base64.StdEncoding.DecodeString(cookie.Value)
        if err != nil {
            continue
        } else {
            copy(sessionKey[:], slice)
        }
    }
    u, _ = getUserBySessionKey(sessionKey)

    // regenerate key. TODO: does that add security?
    rand.Read(sessionKey[:])

    header := make(http.Header)
    header.Add("Set-Cookie", (&http.Cookie{
        Name:     sessionKeyCookieName,
        Value:    base64.StdEncoding.EncodeToString(sessionKey[:]),
        MaxAge:   int(sessionLength.Seconds()),
        HttpOnly: true,
        Domain:   strings.SplitN(r.Host, ":", 2)[0],
    }).String())

    ws, err := upgrader.Upgrade(w, r, header)
    if err != nil {
        if _, ok := err.(websocket.HandshakeError); !ok {
            log.Println(err)
        }
        return
    }

    // do things to `user` so their messages go to where they're needed

    go c.writePump()
    c.readPump()
}

Headers as seen on Firefox network dev tool

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: eSazcZyZKj2dfa2UWSY+a4wThC8=
Access-Control-Allow-Origin: *
Set-Cookie: session-key=RNStK2z2gAsan7DyNKQ+efjyr7c=; Domain=redacted.org; Max-Age=259200; HttpOnly

Am I skipping some step that would allow Safari to store cookies, or is this an issue upstream1?

P.S. I'd really like to retain this approach, since I can use HTTP-only cookies and that mostly ensures that JavaScript has no access to them.


  1. Looks like Gary is having similar issues as well. In short, cookies don't travel back over WebSockets.
like image 296
transistor09 Avatar asked Dec 10 '17 19:12

transistor09


2 Answers

TLDR: it's the HttpOnly flag.


It appears that while some browsers do allow Set-Cookie header in a response for WebSocket connection to have HttpOnly flag, iOS Safari considers the situation as "non-HTTP" and blocks this.

Interestingly, while setting HttpOnly cookies is not possible, HttpOnly cookies are sent in request headers while connecting a WebSocket. This leaves a pair of options:

  • Increase risk and omit HttpOnly;
  • Set your cookies with another plain HTTP request, quite possibly one that doesn't even have a response body.

I'd consider iOS Safari's behavior to be incorrect compared to what's outlined in RFC 6265 Storage model

like image 186
transistor09 Avatar answered Nov 16 '22 02:11

transistor09


If your Set-Cookie header works in other browsers my guess is it's an upstream issue, specifically iOS Safari has the ability to block cookies. By default iOS Safari blocks 3rd party cookies.

Can a webpage in mobile Safari check whether Settings > Safari > Accept Cookies 'From visited' or 'Always' is selected?

If cookies are blocked you can't use them. If you need cookies, detect support by setting a cookie on the login page like enabled=1 and then check for it in /ws handler. If it comes up blank and cookies are blocked you can try redirecting to /please-enable-cookies to ask the user to enable cookies for your site.

Another option is to store signed session data in local storage and include it in each request in the Authorization header. https://jwt.io/

like image 38
AJcodez Avatar answered Nov 16 '22 01:11

AJcodez