Check the login response in DevTools
In the Network panel, select the login or OAuth callback request and open its Cookies tab. The Blocked response cookies filter shows requests whose response cookies were blocked. Hover over the info icon to see the reason.
Docs: developer.chrome.com →
Compare the frontend and API sites
A site is the registrable domain: a Public Suffix List entry plus the label before it. SameSite rules also consider the scheme. Ports are ignored, so a split that is same-site on localhost can be cross-site in production.
Docs: developer.mozilla.org →
Trust the proxy before setting Secure cookies
In Express with express-session, set trust proxy so the session middleware reads X-Forwarded-Proto from your reverse proxy, and keep cookie.secure on.
app.set('trust proxy', 1) // trust first proxy
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: { secure: true }
}))
Docs: github.com →
Serve the frontend and API from one site
Subdomains of one registrable domain are the same site, so SameSite=Lax does not block the cookie between app.example.com and api.example.com. Calls between them are still cross-origin because the hostnames differ, so the credentials and CORS headers in the next step still apply. The cookie must also reach the API host: set it from the API, or set Domain=example.com.
Docs: developer.mozilla.org →
Send and allow credentials on cross-origin API calls
Cross-origin fetch() sends no credentials by default, and browsers ignore Set-Cookie on a CORS response unless the request includes credentials. The server must answer with an explicit origin and Access-Control-Allow-Credentials: true. If the API is on a different site, the cookie also needs SameSite=None with Secure.
fetch('https://api.example.net/login', { method: 'POST', credentials: 'include', body })
// response headers:
// Access-Control-Allow-Origin: https://app.example.com
// Access-Control-Allow-Credentials: true
// Set-Cookie: sid=<value>; Path=/; Secure; SameSite=None
Docs: developer.mozilla.org →
Use Lax instead of Strict for GET callbacks
Lax cookies are sent on cross-site top-level navigations that use a safe method, which excludes POST. That covers an OAuth callback that returns as a GET redirect, where a Strict cookie is not sent. A callback delivered as a POST is not covered by Lax.
Set-Cookie: sid=<value>; Path=/; Secure; SameSite=Lax
Docs: developer.mozilla.org →
Fix or drop the Domain attribute
Leave out Domain to keep the cookie on the host that set it. Or set it to a parent domain that the app and API share, so a cookie from api.example.com also reaches middleware on app.example.com.
Set-Cookie: sid=<value>; Domain=example.com; Path=/; Secure; SameSite=Lax
Docs: developer.mozilla.org →