Fix Access-Control-Allow-Credentials Errors: Wildcard Conflicts & Preflight Failures

Cross-origin requests that send cookies, HTTP Basic/Digest auth, or TLS client certificates require the server to return Access-Control-Allow-Credentials: true. When that header is missing, miscased, or paired with a wildcard origin, the browser silently drops the response. This page is part of Credential Sharing & Security Boundaries, which covers the full credential isolation model browsers enforce.

Exact Console Error This Page Resolves

Access to XMLHttpRequest at 'https://api.example.com/data' from origin
'https://app.example.com' has been blocked by CORS policy: The value of the
'Access-Control-Allow-Origin' header in the response must not be the wildcard
'*' when the request's credentials mode is 'include'.

A second variant appears when the credentials header itself is missing or malformed:

Access to fetch at 'https://api.example.com/data' from origin
'https://app.example.com' has been blocked by CORS policy: Response to
preflight request doesn't pass access control check:
'Access-Control-Allow-Credentials' header is missing or has a value other
than 'true'.

Root Cause

The Fetch Standard, section 3.2.5 defines two hard rules the browser enforces before exposing a credentialed response to JavaScript:

  1. Access-Control-Allow-Credentials must contain the exact string true (case-sensitive). The values True, 1, or yes all fail the check.
  2. Access-Control-Allow-Origin must be a single, exact origin — not *. Wildcards are unconditionally forbidden when credentials are involved because any origin could otherwise read authenticated user data.

The preflight OPTIONS request is always sent without credentials regardless of client configuration. But the preflight response must still include both Access-Control-Allow-Credentials: true and the reflected origin — the browser reads those values to decide whether to proceed with the credentialed main request.

For context on how Core CORS Mechanics & Same-Origin Policy Fundamentals defines the origin tuple that drives this check, see the parent pillar.

Prerequisite State

Before applying the fix, confirm:

Step-by-Step Fix

Step 1 — Remove the wildcard and reflect the requesting origin

Read the Origin request header, validate it against a strict allowlist, and echo it back in the response. Never use regex or substring matching without anchoring — example.com.evil.com would match example.com without anchors.

Express.js:

const ALLOWED_ORIGINS = new Set([
  'https://app.example.com',
  'https://admin.example.com'
]);

app.use((req, res, next) => {
  const origin = req.headers['origin'];
  if (origin && ALLOWED_ORIGINS.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Vary', 'Origin');
  }
  next();
});

Nginx:

map $http_origin $cors_origin {
  default         "";
  "https://app.example.com"   $http_origin;
  "https://admin.example.com" $http_origin;
}

server {
  location /api/ {
    add_header Access-Control-Allow-Origin      $cors_origin always;
    add_header Access-Control-Allow-Credentials true         always;
    add_header Vary                             Origin       always;

    if ($request_method = OPTIONS) {
      add_header Access-Control-Allow-Methods  'GET, POST, PUT, DELETE' always;
      add_header Access-Control-Allow-Headers  'Content-Type, Authorization' always;
      add_header Access-Control-Max-Age        600 always;
      return 204;
    }

    proxy_pass http://backend;
  }
}

The map block evaluates before the location block runs, so $cors_origin is already set when add_header executes. Avoid using Nginx if for header logic — it runs in the rewrite phase and produces unreliable results with add_header.

Step 2 — Add Vary: Origin to every response that reflects an origin

Without Vary: Origin, a CDN or shared proxy may cache the response for one origin and serve it to a different origin. That mismatch causes the credential check to fail for subsequent requests. Omitting Vary: Origin is a silent bug — it will appear to work locally but fail in production behind a cache layer.

Step 3 — Ensure the OPTIONS preflight also carries both credential headers

The fix in step 1 must apply to OPTIONS responses as well. Confirm the preflight returns:

HTTP/2 204
access-control-allow-origin: https://app.example.com
access-control-allow-credentials: true
access-control-allow-methods: GET, POST, PUT, DELETE
access-control-allow-headers: Content-Type, Authorization
vary: Origin

Step 4 — Confirm client-side credential mode

// Fetch API — credentials must be 'include', not 'same-origin' or 'omit'
const response = await fetch('https://api.example.com/data', {
  method: 'POST',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ query: 'example' })
});

credentials: 'same-origin' (the default) only sends credentials to same-origin URLs. Cross-origin APIs require 'include' explicitly.

Preflight Flow Diagram

Access-Control-Allow-Credentials preflight validation flow Sequence diagram showing how the browser validates Access-Control-Allow-Credentials during the OPTIONS preflight before sending a credentialed main request Browser Server JS Code fetch(url, { credentials: 'include' }) OPTIONS /data (no cookies) Origin: https://app.example.com 204 No Content ACAO: https://app.example.com ACAC: true ACAC = true? yes GET /data + cookies attached Request blocked; error thrown no 200 OK (ACAO + ACAC required again) Response exposed to JS

Verification

Run this curl command to inspect the raw preflight response headers:

curl -si -X OPTIONS https://api.example.com/data \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization" \
  | grep -i "access-control\|vary\|http/"

Expected output:

HTTP/2 204
access-control-allow-origin: https://app.example.com
access-control-allow-credentials: true
access-control-allow-methods: GET, POST, PUT, DELETE
access-control-allow-headers: Content-Type, Authorization
vary: Origin

DevTools verification checklist:

Security Boundary Note

Do not accept every Origin value and echo it back unconditionally. Reflecting an arbitrary origin without allowlist validation creates an open CORS endpoint — any authenticated request from any site will succeed, enabling cross-site data theft via a malicious page that the victim’s browser visits. Always validate against a hardcoded allowlist; never build the allowlist from request parameters, query strings, or database values that an attacker can influence.

Cookie attributes interact independently of CORS: a cookie with SameSite=Lax or SameSite=Strict will not be sent on cross-origin requests even when Access-Control-Allow-Credentials: true is correct. Cross-origin auth flows require SameSite=None; Secure on every cookie that must travel cross-origin.

Common Mistakes

Issue Why It Fails
Access-Control-Allow-Credentials: True (capital T) The Fetch Standard requires the exact ASCII string true. Any other casing fails the check silently — the browser treats it as missing.
Setting Access-Control-Allow-Origin: * when credentials: 'include' is used Browsers unconditionally block credentialed responses with wildcard origins. The error appears in the console; the response body is never accessible to JavaScript.
Returning Access-Control-Allow-Credentials: true on the main request but not on the OPTIONS preflight The browser reads the preflight first. If the credential header is absent from OPTIONS, the main request is never sent regardless of the actual response headers.
Omitting Vary: Origin on responses that dynamically reflect origins CDN or shared-cache nodes cache the first response and serve it to other origins. Those origins receive someone else’s reflected Access-Control-Allow-Origin, causing credential failures.

FAQ

Why does the browser block requests when Access-Control-Allow-Credentials is true and Access-Control-Allow-Origin is *?

The Fetch Standard (section 3.2.5) explicitly forbids wildcard origins when credentials are enabled. Allowing any origin to receive credentialed responses would let any site read your authenticated user’s data. Servers must echo back the exact requesting Origin value from an allowlist instead.

Does Access-Control-Allow-Credentials: true also cover Authorization headers?

The header enables transmission of cookies and HTTP authentication credentials. Custom Authorization headers additionally require explicit listing in Access-Control-Allow-Headers to pass the preflight check — they are not covered automatically.

Must Access-Control-Allow-Credentials: true appear on the OPTIONS preflight response?

Yes. The browser evaluates the preflight response before sending the actual credentialed request. If Access-Control-Allow-Credentials: true is absent from the OPTIONS response, the browser aborts the main request without sending it.