The cookies-vs-tokens debate is one of those frontend arguments that generates more heat than light because both sides are right in different contexts. The real question is not "which is better?" — it is "which threat model applies to my app?" Cookies and tokens have different attack surfaces, different browser behaviors, and different tradeoffs for mobile versus web. Here is the 2026 breakdown.
What changed in 2026
- The
SameSite=Strict default in all major browsers killed a large class of CSRF attacks, making cookie-based auth more robust without manual mitigation.
- Partitioned cookies (CHIPS) resolved the third-party cookie blocking problem for legitimate cross-site use cases — relevant for embedded widgets and iframes.
- Browser-native passkeys handle first-factor auth in many apps, but session maintenance after passkey authentication still uses cookies or tokens.
- The "store JWT in localStorage" anti-pattern is now widely documented enough that most security audits flag it immediately.
What each option is
Session cookies: The server creates a session, stores it server-side, and gives the client an opaque session ID in a Set-Cookie header. Subsequent requests send the cookie automatically.
Tokens (JWT): A self-contained signed payload that the client stores and sends manually, usually as Authorization: Bearer <token>.
The boundary blurs when you store a JWT in a cookie — which is a valid and often recommended pattern.
Security comparison
| Threat |
Cookies (HttpOnly) |
Token in localStorage |
Token in memory |
| XSS reads the credential |
Protected |
Vulnerable |
Protected (lost on reload) |
| CSRF (cross-site request) |
Requires SameSite / CSRF token |
Not vulnerable (JS must set header) |
Not vulnerable |
| Credential persists across tabs |
Yes |
Yes |
No (per-tab) |
| Works cross-domain |
Needs CORS + credentials |
Yes |
Yes |
| Mobile/native apps |
Awkward |
Fine |
Fine |
The recommended patterns by app type
Server-rendered web apps (Next.js SSR, Rails, Django)
Use HttpOnly session cookies. The server issues a session after authentication; the cookie is sent automatically and is inaccessible to JavaScript. Pair with SameSite=Lax (default in 2026) and a CSRF token for state-changing requests.
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
Single-page apps (React, Vue, Svelte)
Option A — HttpOnly cookie holding the JWT or session ID. The SPA makes requests with credentials: "include". The cookie is managed by the browser, not by JS.
Option B — JWT in JS memory + HttpOnly refresh token cookie:
// Access token in memory (lost on reload — intentional)
let accessToken = null;
async function refreshAccessToken() {
const res = await fetch("/auth/refresh", {
method: "POST",
credentials: "include", // sends HttpOnly refresh token cookie
});
const data = await res.json();
accessToken = data.access_token; // store in memory only
}
// On every API call:
fetch("/api/data", {
headers: { Authorization: `Bearer ${accessToken}` },
});
The refresh token cookie is HttpOnly and is never readable by JS. The access token lives only in memory and is gone on page reload — a feature, not a bug, for high-security apps.
Mobile and native apps
Use tokens. Store them in the platform secure storage:
- iOS: Keychain
- Android: Android Keystore / EncryptedSharedPreferences
- Electron:
safeStorage API
Do not use AsyncStorage (React Native) for tokens without encryption. Use expo-secure-store or react-native-keychain.
Cookie security flags
Every auth cookie must have the right flags:
Set-Cookie: session=<value>;
HttpOnly; # not readable by JS
Secure; # HTTPS only
SameSite=Lax; # blocks cross-site requests on navigations
Path=/; # scope to your app
Max-Age=86400; # explicit expiry (1 day)
SameSite=Strict is the most secure but breaks OAuth redirect flows (the cookie is not sent on the redirect back to your site). Lax is the right default for most apps.
How to pick
- Server-rendered app (Rails, Django, Laravel)? HttpOnly session cookie. Simplest and safest.
- SPA with your own backend? HttpOnly cookie holding the JWT, or memory token + HttpOnly refresh cookie. Never localStorage.
- SPA calling a third-party API directly? Token in memory with silent refresh. Cookies do not cross domains cleanly.
- Mobile app? Tokens in secure device storage. Cookies are a browser primitive.
- Microservices with many origins? Bearer tokens — cookies do not survive cross-origin requests without
credentials: include plus CORS setup on every service.
Common mistakes
JWT in localStorage. XSS anywhere on the page — even in a third-party script — can read localStorage and exfiltrate the token. HttpOnly cookies are immune to this.
Missing Secure flag. Without it, the cookie is sent over HTTP, exposing it to network sniffing. Always set Secure in production.
Long-lived tokens with no refresh. A stolen access token is valid until it expires. Keep access tokens short (15 minutes); use refresh token rotation to limit the blast radius.
CORS credentials: true with Access-Control-Allow-Origin: *. Browsers reject this combination. If you need credentialed cross-origin requests, you must specify the exact origin.
Not rotating refresh tokens. Issue a new refresh token on every use and invalidate the old one. Rotation means a stolen refresh token is detected when the legitimate client uses it next.
What to skip
- localStorage or sessionStorage for JWT in web apps — the XSS surface is too large.
- Cookies for mobile apps — they work technically but are not the native pattern and add friction.
- Rolling your own CSRF protection when
SameSite=Lax and modern frameworks handle it — but do not skip CSRF tokens entirely for critical state-changing endpoints if you support legacy browsers.
FAQ
Can I put a JWT inside a cookie?
Yes, and it is often the best of both worlds. The JWT is self-contained (stateless validation), and the cookie is HttpOnly (XSS-safe). Just keep the JWT small — cookies have a 4KB limit.
Does SameSite=Strict break OAuth login flows?
Yes. On the OAuth redirect back to your site, the browser treats it as a cross-site navigation and withholds SameSite=Strict cookies. Use Lax for the session cookie; use Strict only for CSRF tokens.
Are tokens better than cookies for APIs?
For machine-to-machine API calls, yes — Bearer tokens are the standard. Cookies are browser primitives; APIs consumed by servers or mobile apps should use tokens.
What is refresh token rotation?
Each time the client exchanges a refresh token for a new access token, the server issues a new refresh token and invalidates the old one. If an attacker reuses a rotated token, the server detects the reuse and can invalidate the whole session.
Where to go next