# Access-Control-Allow-Credentials Header

> Learn how Access-Control-Allow-Credentials controls whether browsers expose responses when credentials (cookies, auth headers) are included in CORS requests.

Source: https://howhttpworks.com/headers/access-control-allow-credentials
Last reviewed: 2026-10-05

> **TL;DR:** If your frontend calls another origin with `credentials: 'include'` (cookies or HTTP auth), the response needs `Access-Control-Allow-Credentials: true` plus an exact `Access-Control-Allow-Origin`, not `*`. Without them, the browser hides the response from JavaScript. The request itself may already have gone out with its cookies, so a missing header is not a security control.

## What is Access-Control-Allow-Credentials?

It's a response header that lets the browser share a response with JavaScript when the request's credentials mode is `include`. The client picks the credentials mode. This header doesn't switch cookie sending on; it only decides whether the page gets to read what comes back. [MDN describes how this differs between preflighted and simple requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Credentials#description).

So a cookie-authenticated GET can succeed on the server while `fetch()` rejects in the browser. Your access log says the operation ran; the JavaScript result says whether the page may read the response.

## How Access-Control-Allow-Credentials Works

For a simple request with no preflight, the browser sends the request, cookies and all, before it has seen any response headers. If the CORS check then fails, JavaScript gets a network error, even though the server may have done the work.

For a preflighted request, the browser first sends an `OPTIONS` request without cookies. That preflight response has to allow credentials before the real request goes out, and the real response has to pass the CORS check too. These rules apply whenever the mode is `include`, even if there happens to be no cookie to send. [Fetch defines the credential-mode checks](https://fetch.spec.whatwg.org/#cors-protocol-and-credentials).

## Syntax

```http
Access-Control-Allow-Credentials: true
```

`true` is the only valid value, and it's case-sensitive, so `True` fails. Writing `false` doesn't help either. To opt out, leave the header off.

## Common Examples

The headers below are trimmed examples. A cookie-authenticated API response includes:

```http
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
Content-Type: application/json
```

Write the origin without a trailing slash. A wildcard origin fails whenever the request's credentials mode is `include`.

For a cross-site session, the login response might also set a cookie like this:

```http
Set-Cookie: session=REPLACE_WITH_SESSION_ID; Path=/; Secure; HttpOnly; SameSite=None
```

`HttpOnly` hides the cookie from `document.cookie`, but the browser still sends it with any eligible fetch. Since the cookie belongs to the API host, the frontend may not be able to see it at all. Check its domain and path in the browser's cookie storage view instead.

## Real-World Scenarios

From a page at `https://app.example.com`, fetch the profile like this:

```javascript
const response = await fetch('https://api.example.com/api/profile', {
  credentials: 'include'
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const profile = await response.json()
```

Leave `Content-Type: application/json` off a GET with no body. It does nothing useful there and it turns a simple request into a preflighted one.

With XMLHttpRequest, set `withCredentials` before calling `send()`:

```javascript
const xhr = new XMLHttpRequest()
xhr.open('GET', 'https://api.example.com/api/profile')
xhr.withCredentials = true
xhr.onload = () => console.log(xhr.status, xhr.responseText)
xhr.onerror = () => console.log('Request could not be read')
xhr.send()
```

See [MDN on withCredentials](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials). The error callback looks the same for a failed CORS check and any other network failure, so open the Network panel to see what actually came back.

Bearer tokens are a different setup. Adding an `Authorization` header yourself means the preflight has to allow it, but it doesn't put the request in `include` mode:

```javascript
await fetch('https://api.example.com/api/profile', {
  credentials: 'omit',
  headers: { Authorization: 'Bearer REPLACE_WITH_TOKEN' }
})
```

Here the preflight needs `Access-Control-Allow-Headers: Authorization`, and `Access-Control-Allow-Credentials` isn't required. Switch to `include` only if the endpoint also relies on cookies the browser manages.

## Security Considerations

CORS permission doesn't override cookie rules. Cross-origin and cross-site also mean different things: `app.example.com` and `api.example.com` over HTTPS are cross-origin but same-site. For a genuinely cross-site cookie, you opt in with `SameSite=None; Secure`, and even then the browser's third-party cookie restrictions may block it. See [MDN's cookie attributes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#attributes).

## Getting Access-Control-Allow-Credentials right

With Express and the `cors` package, register CORS before your authentication and route handlers:

```javascript
const express = require('express')
const cors = require('cors')
const app = express()

app.use('/api', cors({
  origin: ['https://app.example.com', 'https://admin.example.com'],
  credentials: true,
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type', 'Authorization']
}))
// Register authentication and /api route handlers here.
app.listen(3000)
```

The [middleware answers preflights itself](https://expressjs.com/en/resources/middleware/cors/) and adds `Vary: Origin` when it reflects an allowed origin. Authentication is still your job.

## Common Errors and Solutions

If the console complains about a wildcard origin with credentials, compare the request's credentials mode with the `Access-Control-Allow-Origin` the server sent. If the credentials header is missing, check both the OPTIONS response and the real response, including error responses. When you echo the origin back, echo only origins on your allowlist, never whatever the request sent.

A common trap: OPTIONS returns the right headers, but the GET returns 401 without any CORS headers. JavaScript then sees a CORS failure instead of a readable 401. Put CORS middleware ahead of authentication so the client can read `response.status` and handle the 401 (it's still a failed login, just a visible one).

If no `Cookie` header shows up on the request, start with the cookie store and the request itself. `Access-Control-Allow-Credentials` has no say over a cookie the browser rejected for its attributes or blocked by policy.

## Testing

Point this at your own API:

```bash
curl -i -X OPTIONS https://api.example.com/api/profile \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type'
```

Then run the real fetch in the browser and watch the Network panel. curl shows you the headers, but it doesn't enforce CORS the way a browser does. Checking `response.headers.get('Access-Control-Allow-Credentials')` in JavaScript won't help either, because browsers don't expose CORS response headers to scripts by default.

To look at the real response, use a test session you control:

```bash
curl -i https://api.example.com/api/profile \
  -H 'Origin: https://app.example.com' \
  --cookie 'session=REPLACE_WITH_TEST_SESSION'
```

You're handing curl the cookie directly, and curl ignores SameSite and third-party cookie blocking. Compare the headers on an authenticated and an unauthenticated request, then go back to the browser to confirm the cookie is sent and the response is readable.

## Security Risks of Credentials: true

Blocking the read doesn't stop CSRF. A simple request that skips preflight reaches the server and can change state whether or not the page can read the reply. Keep a tight origin allowlist and protect state-changing routes with their own CSRF defenses. Remember too that every allowed origin can read the responses you've opened to it, so a compromised allowed origin has the same access.

## Related Headers

- [Access-Control-Allow-Origin](https://howhttpworks.com/headers/access-control-allow-origin)
- [Access-Control-Allow-Headers](https://howhttpworks.com/headers/access-control-allow-headers)
- [Access-Control-Allow-Methods](https://howhttpworks.com/headers/access-control-allow-methods)
- [Origin](https://howhttpworks.com/headers/origin)
