I am unable to get Safari to successfully apply Set-Cookie
from server responses when using the Fetch API (actually, via the fetch polyfill). The same code works correctly in FF and Chrome (I tested using both native and polyfill fetch
).
credentials: true
;Set-Cookie
header;Does someone know what the problem might be?
I've read through the documentation and gone through many of the closed bug reports. Unless I missed something, I think maybe the problem is with the 'default browser behaviour' dealing with cookies and CORS -- and not with fetch (reading through the polyfill source code, it seems 100% ignorant of cookies). A few bug reports suggest a malformed server response can prevent cookies from being saved.
My code looks like this:
function buildFetch(url, init={}) {
let headers = Object.assign({}, init.headers || {}, {'Content-Type': 'application/json'});
let params = Object.assign({}, init, { credentials: 'include', headers });
return fetch(`${baseUrl}${url}`, params);
}
buildFetch('/remote/connect', {method: 'PUT', body: JSON.stringify({ code })})
.then(response => response.json())
.then(/* complete authentication */)
The actual authorization request is below. I am using cURL to get the exact request/response data, since Safari makes it hard to copy/paste it.
curl 'https://mydevserver:8443/api/v1/remote/connect' \
-v \
-XPUT \
-H 'Content-Type: application/json' \
-H 'Referer: http://localhost:3002/' \
-H 'Origin: http://localhost:3002' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8' \
--data-binary '{"token":"value"}'
* Trying 127.0.0.1...
* Connected to mydevserver (127.0.0.1) port 8443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
* Server certificate: mydevserver
> PUT /api/v1/remote/connect HTTP/1.1
> Host: mydevserver:8443
> Accept: */*
> Content-Type: application/json
> Referer: http://localhost:3002/
> Origin: http://localhost:3002
> User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8
> Content-Length: 15
>
* upload completely sent off: 15 out of 15 bytes
< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: http://localhost:3002
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Api-Key, Device-Key
< Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
< Access-Control-Expose-Headers: Date
< Content-Type: application/json; charset=utf-8
< Content-Length: 37
< Set-Cookie: express:sess=[SESSIONKEY]=; path=/; expires=Fri, 17 Feb 2017 15:30:01 GMT; secure; httponly
< Set-Cookie: express:sess.sig=[SIGNATURE]; path=/; expires=Fri, 17 Feb 2017 15:30:01 GMT; secure; httponly
< Date: Fri, 17 Feb 2017 14:30:01 GMT
< Connection: keep-alive
<
* Connection #0 to host mydevserver left intact
{"some":"normal","response":"payload"}
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:
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);X-Auth
to Access-Control-Allow-Headers
;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);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.