Cookie not being set in Safari
By Flavio Copes
How to fix a cookie not being set in Safari during local development: Safari rejects the Secure attribute on localhost, so drop it and logins work again.
If a cookie is not being set in Safari but works in every other browser, check if you’re sending the Secure attribute over plain http://localhost. Safari refuses to store a Secure cookie on an insecure connection, even on localhost. Chrome and Firefox make an exception for local development. Safari doesn’t.
I was surprised when a login workflow I implemented worked in all browsers except Safari.
Turns out Safari doesn’t allow setting the secure property on a cookie on localhost.
So I had to remove this cookie property, and things worked again.
Why does the Secure attribute exist?
From MDN:
A cookie with the Secure attribute is only sent to the server with an encrypted request over the HTTPS protocol. It’s never sent with unsecured HTTP (except on localhost), which means man-in-the-middle attackers can’t access it easily. Insecure sites (with http: in the URL) can’t set cookies with the Secure attribute.
In other words, Secure makes sure the cookie only travels over encrypted connections. For a session cookie, that’s exactly what you want in production.
Why does it work in Chrome and Firefox?
Also from MDN:
Insecure sites (http:) cannot set cookies with the Secure attribute (since Chrome 52 and Firefox 52). The https: requirements are ignored when the Secure attribute is set by localhost (since Chrome 89 and Firefox 75).
So Chrome and Firefox decided localhost is trusted enough, and they accept Secure cookies over plain http there. Safari applies the rule strictly.
Not sure if this is how things should work and Chrome and Firefox allow this to make our life simpler, or it’s a Safari bug, but that’s how it is.
How to fix it
The part that makes this bug annoying is that nothing fails loudly. The Set-Cookie header is right there in the response, Safari just ignores it. No error in the console. The login flow just doesn’t stick.
Rather than removing Secure everywhere, set it based on the environment. With Express:
res.cookie('session', token, {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production'
})
In development the cookie works in Safari, and in production it keeps the protection.
One case where this fix is not enough: SameSite=None requires Secure. If your flow needs a cross-site cookie, you can’t drop Secure, and you’ll have to serve your local environment over HTTPS, for example with a tool like mkcert.
Related posts about platform: