Demystifying third-party cookies
What are third-party cookies?
A cookie's host scope determines where it can be sent. Whether its use is first-party or third-party depends on the site of the top-level page and the request context, not simply whether two hostnames differ.
An origin consists of scheme, host, and port. For the cookie contexts discussed here, a site uses the scheme and registrable domain. These HTTPS examples illustrate the distinction:
| Page | Request destination | Relationship |
|---|---|---|
https://app.example.com | https://app.example.com/api | Same origin and same site |
https://app.example.com | https://api.example.com | Cross-origin, same-site |
https://app.example.com | https://widget.example.org | Cross-origin, cross-site |
Cookies used by an embedded service on a different site are commonly called third-party cookies. Sharing a parent-domain cookie between same-site subdomains does not by itself make that cookie third-party. See same-site versus same-origin.
Diagnose cookie failures in four parts: host/path scope, SameSite eligibility, the request's credentials mode, and browser privacy policy. For script access to a cross-origin response, check CORS as well.
Viewing cookies in chrome
- Open chrome devtools (
F12orCmd+Option+I) - Navigate to Application tab
- Expand Storage → Cookies
- Select the domain to view cookies
Pro tip: Click on a cookie to see all its attributes (Domain, Path, Expires, HttpOnly, Secure, SameSite, etc.)
Common use cases
Some integrations have used third-party cookies for:
- Embedded or silent sign-in checks; redirect-based SSO does not inherently require third-party cookies
- Embedded content (social media widgets, payment forms)
- Analytics and tracking
- Advertising and retargeting
- Shopping cart persistence across domains
Example scenario: A payment widget at payments.example.org is embedded on shop.example.com. Its cookies are being used in a cross-site context. By contrast, an HTTPS cart shared between shop.example.com and blog.example.com is a same-site subdomain case; it needs the appropriate cookie host scope, not third-party status.
Cookie security attributes
Understanding cookie attributes is essential for security and proper functionality:
HttpOnly
Prevents JavaScript access to cookies, mitigating XSS (Cross-Site Scripting) attacks:
Set-Cookie: sessionId=abc123; HttpOnly
Effect:
// ❌ Cannot access HttpOnly cookies
document.cookie // sessionId won't appear here
// ✅ Only accessible by server
// Sent automatically with HTTP requests
Best practice: Always set HttpOnly for authentication tokens and sensitive cookies.
Secure
Ensures cookies are only transmitted over HTTPS, preventing interception:
Set-Cookie: authToken=xyz789; Secure
Important:
- Required for production environments
- Browsers make a localhost exception to the usual HTTPS requirement. Verify your browser and use HTTPS for production-like cross-site testing
- Mandatory when using
SameSite=None
Domain
Specifies which hosts can receive the cookie:
# Host-only cookie: only the host that set it
Set-Cookie: id=1; Path=/
# Parent domain and its subdomains; a leading dot is ignored
Set-Cookie: id=1; Domain=example.com; Path=/
Key differences:
- Without domain attribute: Cookie sent only to exact domain (no subdomains)
- With domain attribute: Cookie sent to domain AND all subdomains
Security note: Be cautious with Domain scope - broader scope increases attack surface.
Path
Restricts cookies to specific URL paths:
Set-Cookie: pref=dark; Path=/settings
Only sent with requests to /settings, /settings/profile, etc. Not sent to /, /about, etc.
SameSite
The most critical attribute for third-party cookie behavior:
Set-Cookie: token=abc; SameSite=Strict|Lax|None
SameSite values:
| Value | Behavior | Use Case |
|---|---|---|
| Strict | Never sent cross-site | High-security sessions |
| Lax (default) | Same-site requests and cross-site top-level safe-method navigation | Most websites |
| None | Eligible cross-site, subject to browser policy (Secure required) | Embedded iframes, SSO |
Detailed examples:
# Strictest - never leaves your site
Set-Cookie: session=xyz; SameSite=Strict; Secure; HttpOnly
# Balanced - sent when user clicks links from other sites
Set-Cookie: preference=dark; SameSite=Lax
# Required for third-party contexts
Set-Cookie: widget_state=collapsed; SameSite=None; Secure
CORS and third-party cookies
CORS governs whether a script may read a cross-origin response. For a credentialed cross-origin fetch, allow the specific requesting origin and credentials. This is separate from whether cookies are eligible to be sent. Images, iframe navigations, and top-level navigations do not all use the same CORS response-access rules. A simple cross-origin request can be sent even when its response is hidden from the calling script.
Preflight requests
For "complex" requests, browsers send an OPTIONS preflight:
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Server response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
When preflight is triggered:
- Non-simple methods (PUT, DELETE, PATCH)
- Custom headers (except Accept, Accept-Language, Content-Language)
- Content-Type other than
application/x-www-form-urlencoded,multipart/form-data,text/plain
Simple requests
No preflight needed for:
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include',
})
Requirements for third-party cookies
For a cross-origin Fetch request, check the applicable conditions below. SameSite requirements depend on whether the request is also cross-site.
1. Browser settings
Privacy behavior varies by browser, mode, user setting, and whether storage is partitioned. Test with the default settings of the browsers you support instead of asking users to allow tracking globally.
Chrome did not complete the previously announced blanket phaseout in 2024–2025. On 22 April 2025, Google said it would maintain its user-choice approach. Safari restricts third-party cookies, while Firefox's Total Cookie Protection partitions cookies by top-level site. Treat this as dated policy context, not a promise about every browser configuration. Google's announcement, Firefox Total Cookie Protection.
2. Server configuration
For a cookie that needs cross-site eligibility, set SameSite=None; Secure. Same-site subdomain requests do not inherently require None:
Set-Cookie: auth=token123; SameSite=None; Secure; HttpOnly
CORS headers required:
Access-Control-Allow-Origin: https://specific-origin.com
Access-Control-Allow-Credentials: true
⚠️ Security warning: Never use Access-Control-Allow-Origin: * with credentials:
# ❌ Invalid - will fail
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
# ✅ Valid - specific origin
Access-Control-Allow-Origin: https://trusted-site.com
Access-Control-Allow-Credentials: true
3. Client configuration
Fetch API:
fetch('https://api.example.com/user', {
method: 'POST',
credentials: 'include', // Required for cross-origin cookies
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ data: 'value' }),
})
Credentials options:
'omit': Do not send cookies or accept response cookies.'same-origin'(default): Use credentials only for same-origin requests.'include': Allow credentials for cross-origin requests too. Cookie scope, SameSite, and browser privacy restrictions still apply.
Axios:
import axios from 'axios'
// Global configuration
axios.defaults.withCredentials = true
// Or per-request
axios.get('https://api.example.com/user', {
withCredentials: true,
})
// With interceptor for all requests
axios.interceptors.request.use((config) => {
config.withCredentials = true
return config
})
XMLHttpRequest:
const xhr = new XMLHttpRequest()
xhr.open('GET', 'https://api.example.com/data')
xhr.withCredentials = true
xhr.send()
Common issues and debugging
Issue 1: cookies not being set
Symptoms: Set-Cookie header in response, but cookie doesn't appear in browser
Checklist:
- ✅ Is the cookie eligible for this context? Cross-site Fetch needs
SameSite=None; cross-origin/same-site Fetch does not inherently need it. - ✅ Is
Secureattribute set? (required withSameSite=None) - ✅ Is the connection HTTPS, or a supported localhost exception?
- ✅ Is
Access-Control-Allow-Credentials: truein response? - ✅ Is
credentials: 'include'in the request? - ✅ Does the browser allow this cookie in this embedding context, including any partitioning rules?
Debug in chrome devtools:
Network tab → Select request → Response Headers → Set-Cookie
Issues tab → Check for cookie warnings
Issue 2: cookies not sent with request
Symptoms: Cookie exists but isn't sent with cross-origin requests
Checklist:
- ✅ Is
credentials: 'include'set in fetch/axios? - ✅ If the request is cross-site, is the cookie eligible under SameSite and the browser's privacy policy?
- ✅ Is the cookie domain correct?
- ✅ Has the cookie expired?
- ✅ Does the path match the request URL?
Debug:
// Check cookies being sent
fetch('https://api.example.com/debug', {
credentials: 'include',
})
.then((res) => res.json())
.then((data) => console.log('Cookies received by server:', data))
Issue 3: CORS preflight failures
Symptoms: Request fails with CORS error before reaching actual endpoint
Common causes:
# ❌ Missing credentials header
Access-Control-Allow-Origin: https://app.com
# Missing: Access-Control-Allow-Credentials: true
# ❌ Wildcard with credentials
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
# ❌ Insufficient allowed methods
Access-Control-Allow-Methods: GET
# Request uses POST
# ❌ Missing allowed headers
# Missing: Access-Control-Allow-Headers: Content-Type
Fix: Server must respond to OPTIONS with all required headers:
// Express.js example
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin)
res.header('Access-Control-Allow-Credentials', 'true')
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
if (req.method === 'OPTIONS') {
return res.sendStatus(200)
}
next()
})
Privacy-preserving alternatives
1. First-party cookies with subdomains
Set-Cookie: session=xyz; Domain=.example.com
Works across app.example.com, api.example.com, shop.example.com
2. Server-side sessions with a suitable cookie context
// After credential verification; the session ID remains a bearer credential.
app.post('/login', async (req, res) => {
const user = await validateCredentials(req.body)
if (!user) return res.sendStatus(401)
const sessionId = generateSecureId()
sessions.set(sessionId, { userId: user.id })
res.cookie('sid', sessionId, { httpOnly: true, secure: true })
})
A server-side session does not bypass cookie restrictions. Host the session endpoint in an appropriate first-party context or use a redirect-based sign-in flow.
3. Token-based authentication (JWT)
// Store token in Authorization header
fetch('https://api.example.com/user', {
headers: {
Authorization: `Bearer ${token}`,
},
})
Pros: Works cross-origin, no cookies needed
Cons: Vulnerable to XSS if stored in localStorage
4. Cross-origin storage APIs
Set-Cookie: session=xyz; Partitioned; Secure; SameSite=None
Partitioned cookies are scoped to the top-level site. The Storage Access API is a separate mechanism with browser-specific permission and user-activation requirements:
// Run from an eligible embedded document in response to user interaction.
document.requestStorageAccess().then(
() => console.log('access granted'),
() => console.log('access denied')
)
Best practices
1. Minimize third-party cookie usage
// ❌ Avoid if possible
Set-Cookie: tracker=123; SameSite=None; Secure
// ✅ Use first-party when feasible
Set-Cookie: session=abc; SameSite=Lax; Secure; HttpOnly
2. Always set security attributes
Set-Cookie: auth=token;
Secure;
HttpOnly;
SameSite=Strict;
Max-Age=3600;
Path=/;
Domain=example.com
3. Implement proper CORS
// Whitelist specific origins
const allowedOrigins = ['https://app.example.com', 'https://dashboard.example.com']
app.use((req, res, next) => {
const origin = req.headers.origin
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin)
res.vary('Origin')
res.header('Access-Control-Allow-Credentials', 'true')
}
next()
})
4. Use environment-specific configuration
const cookieOptions = {
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax', // Choose 'none' only for a required cross-site context over HTTPS.
httpOnly: true,
}
res.cookie('session', sessionId, cookieOptions)
Conclusion
When a cookie is missing, identify whether the request is same-origin, cross-origin/same-site, or cross-site first. Then check its scope, SameSite eligibility, credentials mode, and browser storage policy. Configure CORS for script access to credentialed cross-origin responses. Changing one layer does not override the others.