Safari not setting CORS cookies using JS Fetch API

Answering my own question.

I find it pretty enraging that this is a "working as intended" behaviour of Safari, though I understand their motivation. XHR (and presumably native fetch when it lands natively) does not support the setting of third-party cookies at all. This failure is completely transparent because it is handled by the browser outside of the scripting context, so client-based solutions are not really going to be possible.

One recommended solution you will find here is to open a window or iframe to an HTML page on the API server and set a cookie there. At this point, 3rd party cookies will begin to work. This is pretty fugly and there is no guarantee that Safari won't at some point close that loophole.

My solution is to basically reimplement an authentication system that does what session-cookies do. Namely:

  1. Add a new header, X-Auth: [token], where [token] is a very small, short-lived JWT containing the information you require for your session (ideally only the user id -- something that is unlikely to mutate during the lifetime of your application -- but definitely not something like permissions if permissions can be changed during the session);
  2. Add X-Auth to Access-Control-Allow-Headers;
  3. During sign-in, set the session cookie and the auth token with the payloads you require (both Safari and non-Safari users will get both the cookie and the auth header);
  4. On the client, look for the X-Token response header and echo it back as an X-Token request header any time it sees it (you could achieve persistence by using local storage -- the token expires, so even if the value lives for years, it can't be redeemed after a certain point);
  5. On the server, for all requests for protected resources, check for the cookie and use it if it exists;
  6. Otherwise (if the cookie is absent -- because Safari didn't send it), look for the header token, verify and decode the token payload, update the current session with the provided info and then generate a new auth token and add it to the response headers;
  7. Proceed as normally.

Note that JWT (or anything similar) is intended to solve a completely different problem and should really never be used for session management because of the "replay" problem (think what could happen if a user had two windows open with their own header-state). In this case, however, they offer the transience and security you normally need. Bottom line is you should use cookies on browsers that support them, keep the session information as tiny as possible, keep your JWT as short-lived as possible, and build your server app to expect both accidental and malicious replay attacks.


FYI, trying this ~18 months later, this solution didn't work for me. Or, it seemed to, intermittently and for some users, which was really weird.

One recommended solution you will find here is to open a window or iframe to an HTML page on the API server and set a cookie there. At this point, 3rd party cookies will begin to work. This is pretty fugly and there is no guarantee that Safari won't at some point close that loophole.

Best guess is that whatever logic Safari is using internally depends on the order you manage to get the cookies, or something more complicated and opaque.

The solution I ultimately settled on, which might be an option if you're hitting this because you're serving a React App from a different host than the API, or otherwise control both sites, was to use DNS:

Our client was being served from www.company-name.com and our API was on company-name.herokuapp.com. By making a CNAME record api.company-name.com --> company-name.herokuapp.com, and using that subdomain of the same domain for the requests from the client to the API, Safari stopped considering it a "third-party" cookie.

The upside is that there's very little code involved, and it's all using well-established stuff... The downside is that you need some control/ownership over the API host if you're going to use https - they need a certificate that's valid for the client domain, or users will get a certificate warning - so this wouldn't work (at least not for something end-user-facing) if the API in question isn't yours or a partner's.