logo

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:

PageRequest destinationRelationship
https://app.example.comhttps://app.example.com/apiSame origin and same site
https://app.example.comhttps://api.example.comCross-origin, same-site
https://app.example.comhttps://widget.example.orgCross-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

  1. Open chrome devtools (F12 or Cmd+Option+I)
  2. Navigate to Application tab
  3. Expand Storage → Cookies
  4. 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:

bash
Set-Cookie: sessionId=abc123; HttpOnly

Effect:

js
// ❌ 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:

js
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:

bash
# 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:

http
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:

bash
Set-Cookie: token=abc; SameSite=Strict|Lax|None

SameSite values:

ValueBehaviorUse Case
StrictNever sent cross-siteHigh-security sessions
Lax (default)Same-site requests and cross-site top-level safe-method navigationMost websites
NoneEligible cross-site, subject to browser policy (Secure required)Embedded iframes, SSO

Detailed examples:

bash
# 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:

http
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
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:

javascript
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:

bash
Set-Cookie: auth=token123; SameSite=None; Secure; HttpOnly

CORS headers required:

bash
Access-Control-Allow-Origin: https://specific-origin.com
Access-Control-Allow-Credentials: true

⚠️ Security warning: Never use Access-Control-Allow-Origin: * with credentials:

http
# ❌ 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:

js
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:

js
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:

js
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:

  1. ✅ Is the cookie eligible for this context? Cross-site Fetch needs SameSite=None; cross-origin/same-site Fetch does not inherently need it.
  2. ✅ Is Secure attribute set? (required with SameSite=None)
  3. ✅ Is the connection HTTPS, or a supported localhost exception?
  4. ✅ Is Access-Control-Allow-Credentials: true in response?
  5. ✅ Is credentials: 'include' in the request?
  6. ✅ Does the browser allow this cookie in this embedding context, including any partitioning rules?

Debug in chrome devtools:

js
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:

  1. ✅ Is credentials: 'include' set in fetch/axios?
  2. ✅ If the request is cross-site, is the cookie eligible under SameSite and the browser's privacy policy?
  3. ✅ Is the cookie domain correct?
  4. ✅ Has the cookie expired?
  5. ✅ Does the path match the request URL?

Debug:

js
// 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:

bash
# ❌ 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:

js
// 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

bash
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

js
// 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)

js
// 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

http
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:

js
// Run from an eligible embedded document in response to user interaction.
document.requestStorageAccess().then(
  () => console.log('access granted'),
  () => console.log('access denied')
)

Best practices

bash
// ❌ 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

bash
Set-Cookie: auth=token;
  Secure;
  HttpOnly;
  SameSite=Strict;
  Max-Age=3600;
  Path=/;
  Domain=example.com

3. Implement proper CORS

js
// 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

js
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.

MDN Set-Cookie reference