Skip to content

Fix: CORS Error with Credentials – Access-Control-Allow-Credentials and Wildcard Origin

FixDevs ·

Quick Answer

How to fix CORS errors when using cookies or Authorization headers, including 'Access-Control-Allow-Credentials' and wildcard origin conflicts.

The Error

You add credentials: 'include' to a fetch call or set withCredentials: true on an XMLHttpRequest/axios request, and the browser blocks the response with one of these errors:

Chrome / Edge (wildcard origin with credentials):

Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000'
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'.

Chrome / Edge (missing Allow-Credentials):

Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: Response to preflight request doesn't pass access
control check: The value of the 'Access-Control-Allow-Credentials' header in the
response is '' which must be 'true' when the request's credentials mode is 'include'.

Firefox:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote
resource at https://api.example.com/data. (Reason: expected 'true' in CORS header
'Access-Control-Allow-Credentials').

Firefox (wildcard):

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote
resource at https://api.example.com/data. (Reason: Credential is not supported if
the CORS header 'Access-Control-Allow-Origin' is '*').

XMLHttpRequest variant:

Access to XMLHttpRequest at 'https://api.example.com/data' from origin
'http://localhost:3000' 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'. The credentials mode of requests
initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

Unlike the basic Access-Control-Allow-Origin missing error, this error specifically involves credentialed requests — requests that carry cookies, HTTP authentication, or TLS client certificates. The server may already have CORS headers set, but they are not configured correctly for the stricter rules that apply when credentials are involved.

Why This Happens

When your frontend sends a cross-origin request with credentials, the browser imposes stricter CORS requirements than it does for non-credentialed requests. This is a security measure: cookies and authentication tokens are sensitive, and the browser needs stronger assurance that the server genuinely intends to share them with the requesting origin.

A credentialed request is any request where:

  • fetch() is called with credentials: 'include'
  • XMLHttpRequest has withCredentials = true
  • axios is configured with withCredentials: true

For these requests, the CORS specification enforces three additional rules that do not apply to regular cross-origin requests:

  1. Access-Control-Allow-Origin must not be *. The server must respond with the exact origin of the requesting page (e.g., http://localhost:3000), not the wildcard.
  2. The response must include Access-Control-Allow-Credentials: true. Without this header, the browser discards the response even if the origin matches.
  3. Access-Control-Allow-Headers and Access-Control-Allow-Methods must not be *. Each allowed header and method must be listed explicitly.

Many developers hit this error because they initially configured CORS with Access-Control-Allow-Origin: * (which works for simple requests), and then later added cookies or an Authorization header to their requests. The wildcard that worked before now triggers a hard failure.

The browser enforces these rules on both the preflight OPTIONS response and the actual response. If either one violates the rules, the request is blocked. For more on how preflight works, see Fix: CORS preflight request blocked.

Fix 1: Set a Specific Origin Instead of Wildcard

The most direct fix is to replace the wildcard * in your Access-Control-Allow-Origin header with the exact origin of your frontend.

Before (broken):

Access-Control-Allow-Origin: *

After (working):

Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true

The origin must match exactly — protocol, host, and port. http://localhost:3000 and http://localhost:5173 are different origins. If you are unsure what origin to use, open your browser’s DevTools, go to the Console, and type window.location.origin.

Fix 2: Dynamic Origin Reflection for Multiple Origins

If your API serves multiple frontends (e.g., http://localhost:3000 during development and https://myapp.com in production), you cannot hardcode a single origin. You also cannot use the wildcard. The solution is to dynamically set the Access-Control-Allow-Origin header based on the incoming request’s Origin header, after validating it against an allowlist.

Express / Node.js (manual):

const allowedOrigins = [
  'http://localhost:3000',
  'http://localhost:5173',
  'https://myapp.com',
  'https://staging.myapp.com'
];

app.use((req, res, next) => {
  const origin = req.headers.origin;

  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
    res.header('Access-Control-Allow-Credentials', 'true');
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  }

  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }

  next();
});

Important: When reflecting the origin dynamically, always add a Vary: Origin header. This tells caches and CDNs that the response changes depending on the Origin request header. Without it, a CDN could cache the response with one origin and serve it to a different origin, breaking CORS.

res.header('Vary', 'Origin');

Never blindly reflect the incoming Origin header without checking it against an allowlist. Doing so effectively turns your credentials-enabled CORS policy into the equivalent of *, defeating the purpose of the restriction. An attacker could create a malicious page that sends credentialed requests to your API and read the responses.

Fix 3: Express cors() Middleware Configuration

The cors npm package handles dynamic origin validation and all the credentials-related headers for you.

npm install cors
const express = require('express');
const cors = require('cors');

const app = express();

app.use(cors({
  origin: ['http://localhost:3000', 'https://myapp.com'],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

app.use(express.json());

app.post('/api/login', (req, res) => {
  // Set a cookie
  res.cookie('session', 'abc123', {
    httpOnly: true,
    secure: true,
    sameSite: 'None'
  });
  res.json({ success: true });
});

When you pass an array to the origin option, the cors middleware automatically checks the incoming Origin against the array and reflects the matching origin back. It also sets Vary: Origin for you.

For more complex validation (regex matching, subdomains), pass a function:

app.use(cors({
  origin: function (origin, callback) {
    // Allow requests with no origin (server-to-server, curl, mobile apps)
    if (!origin) return callback(null, true);

    // Allow any subdomain of myapp.com
    if (origin.endsWith('.myapp.com') || origin === 'https://myapp.com') {
      return callback(null, origin);
    }

    // Allow localhost on any port during development
    if (origin.match(/^http:\/\/localhost:\d+$/)) {
      return callback(null, origin);
    }

    callback(new Error('Not allowed by CORS'));
  },
  credentials: true
}));

If the origin value comes from an environment variable that is undefined, the cors middleware may behave unexpectedly — either allowing all origins or blocking everything. Always verify your environment variables are loaded before initializing the middleware.

Fix 4: Frontend Configuration (fetch, axios, XMLHttpRequest)

The CORS credentials error is a two-sided problem. The server must send the correct headers, and the frontend must explicitly opt in to sending credentials. If you forget the frontend side, cookies and auth headers simply are not sent, even if the server is configured correctly.

fetch:

fetch('https://api.example.com/data', {
  method: 'GET',
  credentials: 'include'  // Sends cookies and accepts Set-Cookie
});

The credentials option has three values:

ValueBehavior
omitNever send or receive cookies (default for cross-origin in some older browsers)
same-originOnly send cookies for same-origin requests (default in modern browsers)
includeAlways send cookies, even for cross-origin requests

axios:

// Per-request
axios.get('https://api.example.com/data', {
  withCredentials: true
});

// Or set globally
axios.defaults.withCredentials = true;

XMLHttpRequest:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data');
xhr.withCredentials = true;
xhr.send();

A common mistake is setting credentials: 'include' on the frontend but forgetting to add Access-Control-Allow-Credentials: true on the server. The browser will send the cookies with the request, but it will block the response because the server did not explicitly acknowledge that credentials are allowed.

Fix 5: Nginx Headers for Credentialed Requests

When nginx acts as a reverse proxy in front of your application server, you can configure it to handle CORS headers for credentialed requests. This is particularly useful when your backend framework does not have built-in CORS support.

server {
    listen 443 ssl;
    server_name api.example.com;

    # Map the Origin header to a variable, only if it's in the allowlist
    set $cors_origin "";
    if ($http_origin = "https://myapp.com") {
        set $cors_origin $http_origin;
    }
    if ($http_origin = "https://staging.myapp.com") {
        set $cors_origin $http_origin;
    }

    location /api/ {
        # Handle preflight
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' $cors_origin;
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
            add_header 'Access-Control-Max-Age' 86400;
            add_header 'Vary' 'Origin';
            return 204;
        }

        # Headers for actual requests
        add_header 'Access-Control-Allow-Origin' $cors_origin always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Vary' 'Origin' always;

        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Use the always parameter on add_header for actual requests so that CORS headers are included even on 4xx and 5xx error responses. Without it, a server error gets reported as a CORS error in the browser, which makes debugging difficult.

Do not set CORS headers in both nginx and your backend application. If the browser receives two Access-Control-Allow-Origin headers, it rejects the response with: "The 'Access-Control-Allow-Origin' header contains multiple values, but only one is allowed." Pick one layer and handle CORS there.

Common Mistake: Blindly reflecting the incoming Origin header without validating it against an allowlist. This effectively turns your credentials-enabled CORS policy into a wildcard, allowing any malicious site to make authenticated requests to your API and read the responses.

Even with perfect CORS headers, cookies may still not be sent or accepted if their attributes are not set correctly for cross-origin use. This is one of the most common reasons credentialed CORS requests fail silently — the request goes through, but the cookie is missing.

SameSite Attribute

The SameSite attribute controls whether a cookie is sent with cross-origin requests:

ValueBehavior
StrictCookie is never sent on cross-origin requests
LaxCookie is sent on top-level navigation GET requests only (default in modern browsers)
NoneCookie is sent on all cross-origin requests

For your API cookies to be included in cross-origin fetch/XHR requests, you must set SameSite=None:

// Express
res.cookie('session', token, {
  httpOnly: true,
  secure: true,       // Required when SameSite=None
  sameSite: 'None',   // Required for cross-origin cookies
  maxAge: 86400000
});

Secure Flag

When SameSite is set to None, the browser requires the Secure flag. This means the cookie will only be sent over HTTPS. If your development environment uses http://localhost, you have two options:

  1. Most modern browsers (Chrome 89+, Firefox 75+) exempt localhost from the Secure requirement. The cookie will work on http://localhost even with Secure; SameSite=None.
  2. If your browser does not exempt localhost, set up a local HTTPS certificate using a tool like mkcert.

Domain Attribute

If your frontend is on myapp.com and your API is on api.myapp.com, set the cookie’s Domain attribute to .myapp.com so it is sent to both:

res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'None',
  domain: '.myapp.com'
});

If the frontend and API are on completely different domains (e.g., myapp.com and myapi.io), the cookie is a third-party cookie. See the next section for why this is increasingly problematic.

Modern browsers are phasing out third-party cookies. If your frontend (https://myapp.com) sets or reads cookies from a different domain (https://api.otherdomain.com), browsers may block those cookies entirely, regardless of your CORS and SameSite configuration.

Chrome has been rolling out third-party cookie restrictions and plans to block them by default. Safari (via Intelligent Tracking Prevention) and Firefox (via Enhanced Tracking Protection) already block third-party cookies in many cases.

Symptoms of third-party cookie blocking:

  • The Set-Cookie header is in the response (visible in DevTools Network tab), but the cookie does not appear in Application > Cookies
  • Subsequent requests do not include the cookie even though credentials: 'include' is set
  • The behavior differs across browsers — it works in Chrome but fails in Safari, or vice versa

Solutions

Move your API to the same domain or a subdomain. This is the most reliable solution. If your frontend is https://myapp.com, host your API at https://api.myapp.com. Cookies on the same registrable domain are first-party cookies and are not subject to third-party blocking.

Use token-based authentication instead of cookies. Store the token in memory (not localStorage for sensitive tokens) and send it in the Authorization header:

// Frontend
fetch('https://api.example.com/data', {
  headers: {
    'Authorization': 'Bearer ' + accessToken
  }
});

This avoids the cookie problem entirely. You still need CORS headers on the server (Access-Control-Allow-Headers must include Authorization), but you do not need Access-Control-Allow-Credentials or any cookie configuration.

Use the Storage Access API (limited support). Some browsers offer the Storage Access API, which allows embedded iframes to request access to third-party cookies. This is not a general solution for API calls.

Fix 8: Proxy to Avoid Cross-Origin Entirely

If cookie and CORS configuration is becoming unmanageable, the cleanest solution is often to remove the cross-origin request entirely by proxying API requests through your own server. When the browser sends the request to the same origin as the frontend, CORS does not apply, and cookies work without any SameSite or third-party restrictions.

Next.js API Routes / Route Handlers:

// app/api/data/route.js (App Router)
export async function GET(request) {
  const response = await fetch('https://api.example.com/data', {
    headers: {
      // Forward any auth from the original request
      'Authorization': request.headers.get('Authorization')
    }
  });

  const data = await response.json();
  return Response.json(data);
}

Your frontend calls /api/data (same origin), and Next.js forwards it to the external API on the server side. No CORS involved. For more on Next.js configuration issues, see Fix: Next.js hydration errors.

Vite dev proxy:

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        cookieDomainRewrite: 'localhost',
        secure: false
      }
    }
  }
});

The cookieDomainRewrite option rewrites the cookie’s Domain attribute so it works on localhost. This is only for development — in production, use a proper reverse proxy or same-domain setup.

Nginx reverse proxy in production:

server {
    listen 443 ssl;
    server_name myapp.com;

    # Frontend static files
    location / {
        root /var/www/myapp;
        try_files $uri $uri/ /index.html;
    }

    # Proxy API requests to backend
    location /api/ {
        proxy_pass https://api.example.com/api/;
        proxy_set_header Host api.example.com;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Both the frontend and API are served from myapp.com. The browser sees everything as same-origin. No CORS headers needed, no cookie issues. If your Docker-based deployment has trouble with this setup, see Fix: Docker Compose up errors for common networking problems.

Fix 9: Wildcard Restrictions on Allow-Headers and Allow-Methods

A detail that catches many developers: when credentials are included, the wildcard * restriction does not only apply to Access-Control-Allow-Origin. It also applies to Access-Control-Allow-Headers and Access-Control-Allow-Methods.

This fails with credentials:

Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: *

This works:

Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS

The browser requires every allowed header and method to be listed explicitly. If you use *, the browser may silently ignore it or reject the preflight depending on the browser version.

In Express with the cors middleware, when credentials: true is set, the middleware automatically handles this by reflecting the requested headers and methods rather than sending *. But if you are setting headers manually, you must list them out.

// Manual header setting — must list explicitly when credentials are used
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://myapp.com');
  res.header('Access-Control-Allow-Credentials', 'true');
  // List each header and method explicitly
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Custom-Header');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
  res.header('Vary', 'Origin');

  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }
  next();
});

Still Not Working?

If you have set the correct origin, added Access-Control-Allow-Credentials: true, and configured your cookies properly but the request still fails, work through these checks:

  1. Inspect the actual response headers. Open DevTools, go to the Network tab, find the request, and check the response headers. Verify that Access-Control-Allow-Origin is set to your exact origin (not *), and that Access-Control-Allow-Credentials is true. If the headers are missing, the problem is on the server side — your CORS middleware may not be running, or another middleware is intercepting the request first.

  2. Check both the preflight and the actual request. The Network tab may show two requests: the OPTIONS preflight and the actual GET/POST. Both must have the correct CORS headers. Click the OPTIONS request and verify its response headers separately. If the preflight passes but the actual request fails, your server may be setting CORS headers only in the preflight handler and not on the regular response.

  3. The cookie is not being set. Look in DevTools under Application > Cookies. If the cookie is not there, check the Set-Cookie header in the response. Look for warning icons in Chrome DevTools — Chrome shows detailed reasons why a cookie was rejected (e.g., missing Secure flag, SameSite issue, domain mismatch). Firefox shows similar warnings in the Console.

  4. The cookie is set but not sent. The cookie may be in the browser but not included in subsequent requests. Verify that credentials: 'include' is set on every request that needs the cookie, not just the login request. Also verify that the cookie’s Path attribute matches the API endpoint path.

  5. Safari requires user interaction. Safari’s Intelligent Tracking Prevention can block third-party cookies even with correct configuration. Safari may require the user to have interacted with the third-party domain directly (e.g., visited api.example.com in a browser tab) before allowing its cookies in a cross-origin context. The most reliable fix for Safari is to use the same domain.

  6. Incognito/private mode has stricter cookie policies. Browsers in private mode often apply even stricter third-party cookie restrictions. If your app works in a normal window but fails in incognito, third-party cookie blocking is likely the cause.

  7. A browser extension is interfering. Ad blockers and privacy extensions can strip cookies, modify CORS headers, or block requests entirely. Test in a clean browser profile with no extensions.

  8. The Vary: Origin header is missing. If a CDN or reverse proxy caches a response without the Vary: Origin header, it may serve a response cached for one origin to a request from a different origin. The Access-Control-Allow-Origin header in the cached response will not match, and the browser will block it. Always include Vary: Origin when dynamically reflecting the origin.

  9. Your load balancer terminates SSL and changes the protocol. If the browser requests https://api.example.com but your load balancer forwards it to your app as http://, the Origin header the app sees might not match what you expect. Check the X-Forwarded-Proto and X-Forwarded-Host headers to reconstruct the correct origin.

  10. The server sends Access-Control-Allow-Credentials: True (capital T). The header value must be exactly the lowercase string true. Some server frameworks capitalize it, which causes browsers to reject it. Verify the exact casing in the response headers.


Related: For the basic CORS error where the Access-Control-Allow-Origin header is missing entirely, see Fix: No Access-Control-Allow-Origin header. For preflight-specific issues, see Fix: CORS preflight request blocked.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles