Cybersecurity / Networking

CORS Missing Access-Control-Allow-Credentials Header on Authenticated Requests

· by DebuggedIt

Quick answer

A CORS error specifically on requests that include credentials (cookies, HTTP authentication) but not on simpler, unauthenticated requests to the same endpoint...

A CORS error specifically on requests that include credentials (cookies, HTTP authentication) but not on simpler, unauthenticated requests to the same endpoint means the server's CORS configuration is missing the specific Access-Control-Allow-Credentials header — required, in addition to the usual origin header, whenever a cross-origin request includes credentials.

The Problem

A request that includes cookies or credentials fails with a CORS error, even though the same endpoint works fine for requests without credentials:

Access to fetch at 'https://api.example.com/user' from origin 'https://app.example.com' 
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 is '' 
which must be 'true' when the request's credentials mode is 'include'.

Why It Happens

Browsers enforce an additional, stricter requirement specifically for cross-origin requests that include credentials: the server must explicitly respond with Access-Control-Allow-Credentials: true, on top of the usual Access-Control-Allow-Origin header — without this specific additional header present and set to exactly true, the browser blocks the response even if the origin itself would otherwise be allowed. This requirement exists as an additional safety measure specifically for credentialed requests, since these carry the user's actual authentication state and therefore warrant stricter, more explicit server-side opt-in.

The Fix

Add the credentials header explicitly on the server side, alongside your existing CORS origin configuration:

// Example: Express with the cors package
app.use(cors({
  origin: 'https://app.example.com',
  credentials: true,
}));

Critically, when using credentials, Access-Control-Allow-Origin cannot be a wildcard (*) — it must be a specific, explicit origin, since browsers specifically disallow combining a wildcard origin with credentialed requests as an additional security safeguard:

// This combination is invalid and will be rejected by the browser:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

// This is the required, valid combination:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

If your application needs to support multiple specific allowed origins (rather than just one fixed origin), dynamically reflect the actual requesting origin back in the response header, rather than trying to use a wildcard, which credentialed requests don't permit:

app.use(cors({
  origin: (origin, callback) => {
    const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
}));

On the client side, ensure the request itself is actually configured to include credentials — the server-side header alone isn't sufficient if the client's own request doesn't opt into sending credentials in the first place:

fetch('https://api.example.com/user', {
  credentials: 'include',
});

credentials: 'include' is required specifically for cross-origin requests that need to send cookies or other credentials — without it on the client side, the browser won't send credentials at all regardless of how the server's own CORS headers are configured, meaning both the client-side credentials: 'include' and the server-side Access-Control-Allow-Credentials: true need to be correctly configured together for credentialed cross-origin requests to actually succeed.

Still Not Working?

If you've correctly configured both the client-side credentials: 'include' and the server's Access-Control-Allow-Credentials: true with a specific (non-wildcard) origin, but cookies still don't seem to be sent or received correctly, check the cookie's own SameSite attribute, since this is a separate, additional requirement specifically for cross-origin cookie usage — a cookie set with SameSite=Strict or even the default SameSite=Lax in some browsers won't be sent on genuinely cross-origin requests at all, requiring SameSite=None; Secure specifically for cookies that need to work across genuinely different origins:

Set-Cookie: session=abc123; SameSite=None; Secure; HttpOnly

Note that SameSite=None requires the Secure flag as well (meaning the cookie will only be sent over HTTPS) — this combination is mandatory in modern browsers, and omitting Secure while setting SameSite=None causes the cookie to be rejected entirely rather than simply falling back to more restrictive default behavior.