{"post":{"slug":"cookies-vs-tokens-explained-2026","title":"Cookies vs tokens explained in 2026: which to use for auth sessions","excerpt":"Cookies and tokens are both valid ways to maintain authentication state, but they have different security tradeoffs. Here is the honest 2026 comparison for web and mobile apps.","category":"code","date":"March 10, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">T</span>he 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.\n\n## What changed in 2026\n\n- **The `SameSite=Strict` default** in all major browsers killed a large class of CSRF attacks, making cookie-based auth more robust without manual mitigation.\n- **Partitioned cookies (CHIPS)** resolved the third-party cookie blocking problem for legitimate cross-site use cases — relevant for embedded widgets and iframes.\n- **Browser-native passkeys** handle first-factor auth in many apps, but session maintenance after passkey authentication still uses cookies or tokens.\n- **The \"store JWT in localStorage\" anti-pattern** is now widely documented enough that most security audits flag it immediately.\n\n## What each option is\n\n**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.\n\n**Tokens (JWT):** A self-contained signed payload that the client stores and sends manually, usually as `Authorization: Bearer <token>`.\n\nThe boundary blurs when you store a JWT *in* a cookie — which is a valid and often recommended pattern.\n\n## Security comparison\n\n| Threat | Cookies (HttpOnly) | Token in localStorage | Token in memory |\n| --- | --- | --- | --- |\n| XSS reads the credential | Protected | Vulnerable | Protected (lost on reload) |\n| CSRF (cross-site request) | Requires SameSite / CSRF token | Not vulnerable (JS must set header) | Not vulnerable |\n| Credential persists across tabs | Yes | Yes | No (per-tab) |\n| Works cross-domain | Needs CORS + credentials | Yes | Yes |\n| Mobile/native apps | Awkward | Fine | Fine |\n\n## The recommended patterns by app type\n\n### Server-rendered web apps (Next.js SSR, Rails, Django)\n\nUse 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.\n\n```http\nSet-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/\n```\n\n### Single-page apps (React, Vue, Svelte)\n\nOption 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.\n\nOption B — JWT in JS memory + HttpOnly refresh token cookie:\n\n```javascript\n// Access token in memory (lost on reload — intentional)\nlet accessToken = null;\n\nasync function refreshAccessToken() {\n  const res = await fetch(\"/auth/refresh\", {\n    method: \"POST\",\n    credentials: \"include\",  // sends HttpOnly refresh token cookie\n  });\n  const data = await res.json();\n  accessToken = data.access_token;  // store in memory only\n}\n\n// On every API call:\nfetch(\"/api/data\", {\n  headers: { Authorization: `Bearer ${accessToken}` },\n});\n```\n\nThe 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.\n\n### Mobile and native apps\n\nUse tokens. Store them in the platform secure storage:\n\n- iOS: Keychain\n- Android: Android Keystore / EncryptedSharedPreferences\n- Electron: `safeStorage` API\n\nDo not use `AsyncStorage` (React Native) for tokens without encryption. Use `expo-secure-store` or `react-native-keychain`.\n\n## Cookie security flags\n\nEvery auth cookie must have the right flags:\n\n```\nSet-Cookie: session=<value>;\n  HttpOnly;          # not readable by JS\n  Secure;            # HTTPS only\n  SameSite=Lax;      # blocks cross-site requests on navigations\n  Path=/;            # scope to your app\n  Max-Age=86400;     # explicit expiry (1 day)\n```\n\n`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.\n\n## How to pick\n\n1. **Server-rendered app (Rails, Django, Laravel)?** HttpOnly session cookie. Simplest and safest.\n2. **SPA with your own backend?** HttpOnly cookie holding the JWT, or memory token + HttpOnly refresh cookie. Never localStorage.\n3. **SPA calling a third-party API directly?** Token in memory with silent refresh. Cookies do not cross domains cleanly.\n4. **Mobile app?** Tokens in secure device storage. Cookies are a browser primitive.\n5. **Microservices with many origins?** Bearer tokens — cookies do not survive cross-origin requests without `credentials: include` plus CORS setup on every service.\n\n## Common mistakes\n\n**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.\n\n**Missing `Secure` flag.** Without it, the cookie is sent over HTTP, exposing it to network sniffing. Always set `Secure` in production.\n\n**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.\n\n**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.\n\n**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.\n\n## What to skip\n\n- **localStorage or sessionStorage for JWT** in web apps — the XSS surface is too large.\n- **Cookies for mobile apps** — they work technically but are not the native pattern and add friction.\n- **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.\n\n## FAQ\n\n**Can I put a JWT inside a cookie?**\nYes, 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.\n\n**Does SameSite=Strict break OAuth login flows?**\nYes. 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.\n\n**Are tokens better than cookies for APIs?**\nFor 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.\n\n**What is refresh token rotation?**\nEach 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.\n\n## Where to go next\n\n- [API authentication explained in 2026](/blog/api-authentication-explained-2026)\n- [Authorization explained in 2026](/blog/authorization-explained-2026)\n- [CORS deep dive in 2026](/blog/cors-deep-dive-explained-2026)","hue":316,"metaTitle":"Cookies vs Tokens 2026: Auth Session Best Practices","metaDescription":"Cookies vs tokens for authentication in 2026. HttpOnly cookies, localStorage risks, SPA token patterns, and when each approach is the right choice.","keywords":["cookies vs tokens 2026","jwt in cookies vs localStorage","httponly cookie auth","spa authentication 2026","token storage best practices","csrf vs xss auth","refresh token cookie","bearer token vs session cookie","web authentication 2026","secure token storage"],"tldr":["**HttpOnly cookies are the safer default for web apps** — they are inaccessible to JavaScript, eliminating the XSS token-theft risk that plagues localStorage.","**Tokens in memory work for SPAs** — keep the access token in JS memory (not localStorage), use an HttpOnly cookie for the refresh token, refresh silently on expiry.","**Cookies require CSRF protection; tokens require XSS protection** — neither is inherently more secure; the threat model differs.","**Mobile and native apps should use tokens** — cookies are a browser concept; Bearer tokens in secure device storage are the right fit.","**Skip:** storing JWTs in localStorage in production; XSS attacks can read and exfiltrate them silently."],"readTime":"5 min read","wordCount":1118},"related":[{"slug":"jwt-vs-sessions-2026","title":"JWT vs sessions in 2026: the honest authentication comparison","excerpt":"JWTs and sessions are both valid — the wrong choice creates security holes or scalability headaches. Here is the 2026 decision guide with concrete tradeoffs.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":312},{"slug":"oauth-vs-jwt-2026","title":"OAuth vs JWT Explained 2026: Protocol vs Token Format","excerpt":"OAuth and JWT get compared as if they are rivals, but one is an authorization framework and the other is a token format. Here is the 2026 guide to what each is, how they work together, and common mistakes.","category":"code","date":"June 30, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":240},{"slug":"api-authentication-explained-2026","title":"API authentication explained in 2026: keys, JWTs, and OAuth compared","excerpt":"API authentication is the first line of defense for every service you expose. This is the 2026 guide to API keys, JWTs, and OAuth — with real tradeoffs.","category":"code","date":"March 6, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":312}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"concurrency-explained-2026","title":"Concurrency explained in 2026: how programs do multiple things at once","excerpt":"Concurrency is about managing multiple tasks in overlapping time — not necessarily simultaneously. Getting it right in 2026 means knowing which model fits your workload.","category":"code","date":"March 10, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":314},"older":{"slug":"database-indexing-explained-2026","title":"Database indexing explained in 2026: what every developer must know","excerpt":"Bad indexes cause more production incidents than almost any other root cause. Here is the 2026 guide to choosing, building, and maintaining the right ones.","category":"code","date":"March 10, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":318}}