---
title: "Authentication — Cookie Strategy"
aliases: [payload-cookies, cookie-auth, csrf-prevention]
tags: [payloadcms, authentication, cookies, csrf, security]
sources: [raw/authentication__cookies.md]
created: 2026-05-15
updated: 2026-05-15
---

## Overview

Payload uses **HTTP-only cookies** for browser-based authentication. Cookies are set on `login`, `logout`, `refresh`, and `me` operations and are accessible via `req.user` in [[wiki/payloadcms/rest-api|Access Control]] and Hooks.

---

## Automatic vs. Manual Inclusion

| Scenario | Cookie Behavior |
|----------|----------------|
| Browser navigates directly to API domain | Included automatically |
| `fetch` / XHR from JS | Must pass `credentials: 'include'` |

```ts
const response = await fetch('http://localhost:3000/api/pages', {
  credentials: 'include',
})
```

---

## CSRF Risk

HTTP-only cookies are included by the browser for **any request to your domain**, regardless of the originating page. A malicious third-party site can exploit this:

```ts
// malicious-intent.com — makes authenticated request on victim's behalf
const data = await fetch('https://payload-finances.com/api/me', {
  credentials: 'include',
}).then(r => r.json())
```

---

## CSRF Prevention — `csrf` Whitelist

Add trusted frontend origins to `csrf` in `payload.config.ts`. `serverURL` is always included automatically.

```ts
// payload.config.ts
import { buildConfig } from 'payload'

export default buildConfig({
  serverURL: 'https://my-payload-instance.com',
  csrf: [
    'https://your-frontend-app.com',
    'https://your-other-frontend-app.com',
  ],
  collections: [/* ... */],
})
```

---

## Cross-Domain Authentication

Cookies are **same-site by default** — a frontend on a different domain won't receive them.

### Option 1 — Use Subdomains (preferred)
Cookies cross subdomains freely: `api.example.com` ↔ `example.com`.

### Option 2 — Configure Cookie Flags

```ts
{
  slug: 'users',
  auth: {
    cookies: {
      sameSite: 'None', // allow cross-domain
      secure: true,     // HTTPS only
      // httpOnly: true is always set by Payload
    }
  },
  fields: [],
}
```

> **Gotcha:** `secure: true` fails on `http://localhost`. Use `secure: process.env.NODE_ENV === 'production'` for local dev.

When using `sameSite: 'None'`, replace any `cors` wildcard (`*`) with an explicit domain list.

---

## Key Takeaways

- HTTP-only cookies are set automatically on auth operations; JS can't read them (XSS protection).
- `fetch` calls must include `credentials: 'include'` to send cookies.
- CSRF is mitigated by the `csrf` whitelist in `payload.config.ts`.
- Cross-domain auth: prefer subdomains; fall back to `sameSite: 'None'` + `secure: true` + explicit CORS domains.
- `secure: true` must be conditional — it breaks `http://localhost`.

---

## Related

- [[wiki/payloadcms/authentication-api-keys|Authentication — API Keys]] — non-cookie auth for third-party integrations
- [[wiki/payloadcms/rest-api|REST API]] — how auth headers/cookies apply to REST endpoints
- [[wiki/payloadcms/production|Production & Performance]] — CORS and HTTPS setup for production

---

## Sources

- `raw/authentication__cookies.md`
- https://payloadcms.com/docs/authentication/cookies
