skip to content

A cross-origin fetch to your API worked until you added credentials: 'include' so the session cookie would be sent; now the browser rejects the response and complains about the wildcard in Access-Control-Allow-Origin. What must the API return instead, and why does '*' stop being acceptable once credentials are involved?

level: middleimportance: must knowfreq 68%

answer

  1. cookies make the response user-specific
  2. two gates: attach, then read
  3. the star means everyone can read
  4. exact origin string must be echoed
  5. reflecting Origin needs an allow-list

basics

~20 s

A credentialed cross-origin response must name the exact calling origin in Access-Control-Allow-Origin and also send Access-Control-Allow-Credentials: true. The wildcard is banned there because '*' means any site may read the response — which, with the user's cookies attached, would expose their logged-in data to every origin.

solid answer

~50 s

With `credentials: 'include'`, the browser attaches the user's cookies (and any HTTP auth) to the cross-origin request, so the response is now user-specific. For the page to read it the server must return `Access-Control-Allow-Credentials: true` and echo the caller's exact origin in `Access-Control-Allow-Origin` — `*` is rejected in credentialed mode, and so are wildcards in `Access-Control-Allow-Headers` and `Access-Control-Allow-Methods`. The reason is that `*` means "any origin may read this"; combined with ambient cookies, a lazily wildcarded endpoint would let any site on the web fetch a logged-in user's data and read it. Forcing the server to name one origin makes that an explicit decision per caller. Note two independent gates: the browser must be willing to *attach* the cookie in a cross-site context, and the server must permit the *read*. Failing the first sends an unauthenticated request; failing the second blocks a response that was computed correctly.

go deeper

for a junior

Know that sending cookies cross-origin needs credentials: 'include' on the fetch call, and that the API must then name your exact origin rather than using a wildcard.

for a middle

Explain both required response headers, the literal string match on the origin, and the reasoning: a wildcard claims the response is public, which stops being true once the user's session shaped it.

for a senior

Demonstrate the split diagnosis — an empty Cookie header on the request is an attachment problem, a blocked response with a 200 in the Network panel is a permission problem — and flag unconditional origin reflection as a real vulnerability.

for a principal

Own where the allow-list of credentialed origins lives, how preview and per-branch deployment origins get in without weakening it, and whether cookie-based cross-site auth is worth keeping given browser restrictions on third-party cookies.

## Two separate gates A credentialed cross-origin call has to clear two doors, and confusing them is the usual source of a wasted afternoon. **Gate one: will the browser attach the credential?** By default `fetch` uses `credentials: 'same-origin'`, meaning cookies ride along on same-origin requests and are omitted cross-origin. Setting `credentials: 'include'` asks for them cross-origin too. (`XMLHttpRequest` spells the same request as `xhr.withCredentials = true`.) Whether the cookie is actually attached also depends on how the server originally set it — a cookie sent in a cross-site context must be marked `SameSite=None` and `Secure`, and browsers additionally apply third-party cookie restrictions that may drop it regardless. If this gate fails you do not get a CORS error at all; you get a clean 401 or an anonymous response, because the API simply never saw a session. **Gate two: may the page read the response?** This is where the wildcard rule bites, and it produces a console message rather than a status code. ## What the response must contain For a credentialed request the browser requires both of: - `Access-Control-Allow-Credentials: true` - `Access-Control-Allow-Origin: <the exact origin string the browser sent>` ``` Origin: https://app.example.com (request, set by the browser) Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Credentials: true ``` The match is a literal string comparison. `https://app.example.com` does not match `https://app.example.com/` with a trailing slash, nor `http://app.example.com`, nor a different port. And in credentialed mode `*` is not merely discouraged, it is treated as no permission at all — as are `*` values for `Access-Control-Allow-Headers` and `Access-Control-Allow-Methods`. ## Why the wildcard is banned here `Access-Control-Allow-Origin: *` is a public statement: *any* origin may read this resource. That is a reasonable thing to say about a public JSON feed, because the response is identical for everyone and contains nothing the reader could not fetch themselves. Add cookies and the statement becomes false. The response is no longer public — it is *this user's* data, computed from the session the browser attached automatically. If the wildcard were honoured, any page the user visited could run `fetch('https://api.example.com/me', { credentials: 'include' })` and read their profile, their inbox, their account balance. The user's browser supplies the authentication just by being logged in; the wildcard would supply the read permission. Banning that combination forces the server to name each origin it trusts with a user's session, which is a decision someone has to make consciously rather than one that falls out of a permissive default. This is also why the reflexive fix — "echo whatever is in the `Origin` header back" — deserves care. Reflecting the origin unconditionally *is* a wildcard, written in a way that also satisfies the credentialed rule; it re-opens exactly the hole the rule closes. Reflection is only safe when it is gated on an allow-list of origins you actually trust. ## Diagnosing it from the frontend The symptom pattern is distinctive: the call worked, you turned on credentials, and now the console names the wildcard while the Network panel shows the request completing normally with a 200. That combination says the server computed a fine response and the browser refused to hand it over. Things to check, in order: 1. Does the response carry `Access-Control-Allow-Credentials: true` at all? Many CORS middlewares require it to be enabled explicitly. 2. Is `Access-Control-Allow-Origin` the exact string in your `Origin` request header — no trailing slash, right scheme, right port? 3. Was a cookie actually sent? Look at the request's headers in the Network panel. An empty `Cookie` header means gate one failed, and no amount of CORS configuration will help. 4. Are you sure you need credentials? If the API authenticates with a bearer token in an `Authorization` header rather than a cookie, you do not need `credentials: 'include'` — but the header itself will make the browser check permission in advance, which is a different conversation. ## The pattern to remember Uncredentialed cross-origin reads are about a *resource* being public. Credentialed cross-origin reads are about a *relationship* between two specific origins. The wildcard can only express the first, which is precisely why the browser refuses to let it stand in for the second.

  • Instead of listing origins, a colleague reflects the incoming Origin header back in Access-Control-Allow-Origin so credentialed calls work. What is wrong with that?
    Unconditional reflection is a wildcard that happens to satisfy the credentialed rule, so it grants every site on the web read access to logged-in responses — the exact outcome the wildcard ban exists to prevent. Reflection is only acceptable when the incoming origin is first checked against an allow-list and anything else gets no permission header at all.
  • The Network panel shows the request went out with credentials: 'include' but the Cookie header is empty. Where do you look?
    That is the attach gate failing, not CORS. In a cross-site context the cookie must have been set with `SameSite=None` and `Secure`, and it can still be dropped by the browser's third-party cookie restrictions or by the user's settings. Check how the cookie was set and whether the API is on a genuinely different site; changing CORS response headers will not make an omitted cookie reappear.
  • Does credentials: 'include' change anything for an API that authenticates with a bearer token instead of a cookie?
    No. Bearer tokens travel in an `Authorization` header your code sets explicitly, so nothing ambient is attached and the credentialed-mode rules never engage — a wildcard Allow-Origin stays legal. The header does have its own consequence, though: it is not on the safelist, so the browser will check permission with the server before sending the real request.

saying these in an interview costs you the question

  • Says the wildcard is merely bad practice, not rejected
  • Forgets Access-Control-Allow-Credentials must also be true
  • Reflects any Origin back with no allow-list
  • Confuses cookies not being sent with the response being blocked
  • Thinks credentials: 'include' overrides the cookie's own attributes

context