Cross-Site Cookies and the Third-Party Cookie Phase-Out
Why a perfectly configured SameSite=None cookie still fails for a large share of your users.
You have done everything right: SameSite=None; Secure, exact-origin CORS, credentials: 'include'. It works in Chrome on your machine. It fails for a real slice of your users — and you cannot fix it from the server.
Why: it is a third-party cookie
When a page on abc.com triggers a request to dfg.com, the dfg.com cookie is third-party in the browser's eyes. It is structurally identical to a tracking cookie, and browsers cannot distinguish "my API session" from "an ad network following me".
Where each browser stands
- Safari (ITP) — blocks third-party cookies by default, and has for years. Your cross-site session simply does not exist there.
- Firefox (Total Cookie Protection) — partitions them:
dfg.comgets a separate cookie jar per top-level site. It works withinabc.com, but a session established elsewhere is invisible. - Chrome — has been restricting third-party cookies through a long, repeatedly revised programme. Users can disable them today, and enterprise policies often already do.
- Every browser in private/incognito mode — far more aggressive by default.
CHIPS: partitioned cookies
The Partitioned attribute ("Cookies Having Independent Partitioned State") is the sanctioned path forward. Set-Cookie: sid=…; SameSite=None; Secure; Partitioned gives your cookie a jar per top-level site. It preserves the legitimate embedded use case and removes the tracking one.
The consequence to understand: your dfg.com session cookie under abc.com is a different cookie from the one under xyz.com. If your product depends on one login being shared across several unrelated parent sites, partitioning breaks that by design.
What to do about it
- Move to the same site.
api.abc.com, or proxy throughabc.com/api. Cookies become first-party and all of this evaporates. Best option whenever you control the DNS. - Use bearer tokens. No cookie, no partitioning, no blocking. Best option when the frontend genuinely cannot share a site with the API.
- Adopt
Partitionedif you must stay cross-site with cookies, and accept per-parent-site sessions.
Test it deliberately
Chrome with third-party cookies blocked, and Safari, must be part of your test matrix — not a bug report you receive later.
Example
# The classic cross-site session cookie — blocked in Safari, partitioned in Firefox
Set-Cookie: sid=8f2b...; HttpOnly; Secure; SameSite=None; Path=/
# CHIPS: opt in to a per-top-level-site jar
Set-Cookie: sid=8f2b...; HttpOnly; Secure; SameSite=None; Partitioned; Path=/
# Under abc.com and under xyz.com these are now two DIFFERENT cookies.
# One login no longer follows the user between unrelated parent sites.
# The alternative that avoids the whole category:
# frontend https://abc.com
# API https://api.abc.com → same site → SameSite=Lax works normallyWhen to use it
- An embedded chat widget loses its session in Safari and switches to a token held in the widget's own memory instead of a cookie.
- A SaaS moves its API from api-provider.com to api.customer-domain.com via a CNAME so cookies are first-party for each tenant.
- A team adds Partitioned to their widget cookie, accepting that a user must sign in separately on each host site.
More examples
Detecting the block instead of guessing
Telling the user what is wrong beats an endless spinner. A probe endpoint pair also turns 'it does not work for me' tickets into a one-line answer.
// After a successful cross-site login, verify the cookie actually took.
async function loginAndVerify(email, password) {
const login = await fetch('https://dfg.com/login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!login.ok) throw new Error('invalid_credentials');
// The real test: does a second call see the session?
const me = await fetch('https://dfg.com/api/me', { credentials: 'include' });
if (me.status === 401) {
// Login succeeded, session did not persist → the cookie was rejected.
throw new Error(
'Your browser is blocking third-party cookies, so we cannot keep you ' +
'signed in. Sign in at dfg.com directly, or switch to token mode.'
);
}
return (await me.json()).user;
}
// Also useful for a support page:
async function thirdPartyCookiesBlocked() {
await fetch('https://dfg.com/cookie-probe/set', { credentials: 'include' });
const r = await fetch('https://dfg.com/cookie-probe/check', { credentials: 'include' });
return !(await r.json()).seen;
}Same-site by DNS: the change that removes the problem
One CNAME removes an entire category of intermittent, browser-dependent bugs. When you control both domains, this is nearly always the right call.
# BEFORE — cross-site, third-party cookies, all of the above
frontend https://abc.com (Vercel)
API https://dfg.com (your servers)
# AFTER — same site, first-party cookies, SameSite=Lax works everywhere
frontend https://abc.com (Vercel)
API https://api.abc.com (CNAME → your servers / load balancer)
# DNS
api.abc.com. 300 IN CNAME lb-1234.eu-west-1.elb.amazonaws.com.
# Cookie becomes ordinary again
Set-Cookie: sid=8f2b...; HttpOnly; Secure; SameSite=Lax; Path=/
# CORS is still needed (different ORIGIN), but it is now the easy kind:
Access-Control-Allow-Origin: https://abc.com
Access-Control-Allow-Credentials: true
# What you no longer need: SameSite=None, Partitioned, third-party cookie
# support, or an explanation for Safari users.
Discussion