Single page applications pushed many teams from server-side session cookies to bearer tokens held in JavaScript, and that move quietly traded a CSRF problem for a token theft problem that most front ends are not built to handle. The choice between an HttpOnly session cookie and a bearer access token sent in the Authorization header decides what an XSS bug can steal, whether CSRF defences are needed, how logout and revocation work and how far a stolen credential travels. The IETF's guidance on OAuth for browser apps, published as RFC 10017 (BCP 212) in August 2026, now ranks the options explicitly, with a Backend for Frontend (BFF) at the top. This guide compares the two models, explains the hybrid patterns between them and gives decision rules, settings and metrics for web, SPA and API teams.
Key takeaways
- Cookies are sent by the browser, bearer tokens by your code. That one difference drives the whole comparison: cookies need CSRF defences, tokens readable by JavaScript need XSS-proof storage, which does not exist.
- For browser front ends, keep tokens out of the browser. RFC 10017 ranks the BFF pattern, where the browser holds only an HttpOnly session cookie, as the most secure option for browser-based OAuth apps.
- Bearer means "whoever holds it". RFC 6750 defines a bearer token as usable by any party in possession of it; sender-constraining with DPoP (RFC 9449) or mTLS limits replay.
- Both are stolen after login. Infostealers take cookies and tokens alike and replay them past MFA, so lifetime, binding and revocation matter as much as the login.
- Bearer tokens remain right for APIs and machine clients. Mobile apps, services and agents calling APIs have no browser cookie jar and should use short-lived, audience-restricted tokens.
What each one is
A session cookie carries an opaque session identifier that points to state held on the server. The browser attaches it automatically to every request to the matching site, and an HttpOnly cookie cannot be read by page scripts through document.cookie. The server can end the session at any time by deleting its record. The OWASP Session Management Cheat Sheet asks for at least 64 bits of entropy from a cryptographically secure generator, and session ID regeneration after login and any privilege change.
A bearer token is a credential the client attaches itself, usually an OAuth access token in the header Authorization: Bearer <token>. RFC 6750 section 1.2 defines it as a token that any party in possession of it can use in the same way as anyone else in possession of it, with no proof of a key. Tokens are often JWTs that the API validates locally, which avoids a session store but makes early revocation harder. See the JWT Security Guide.
The terms overlap in practice: a cookie can carry a JWT, and a bearer token can be opaque. The comparison that matters is who attaches the credential (the browser automatically, or your JavaScript explicitly) and where it is stored (an HttpOnly cookie jar, or memory and web storage reachable from script).
Side-by-side comparison
| Dimension | HttpOnly session cookie | Bearer token held by JavaScript |
|---|---|---|
| Who attaches it | Browser, automatically, on matching requests | Application code, in the Authorization header |
| Readable by page script | No (HttpOnly) | Yes, wherever it is stored |
| Impact of XSS | Attacker can act through the victim's browser while the page is open, but cannot export the cookie | Attacker can copy the token and replay it from anywhere until it expires |
| CSRF exposure | Yes, needs SameSite plus CSRF tokens or equivalent | No ambient credential, so classic CSRF does not apply |
| Revocation | Immediate, delete the server-side session | Hard for self-contained JWTs until expiry; needs introspection, short lifetimes or deny lists |
| Cross-domain APIs | Limited to the cookie's site; needs CORS with credentials | Works across origins and APIs by design |
| Server state | Session store required | Can be stateless |
| Non-browser clients | Awkward | Natural fit for mobile, CLI, service and agent clients |
| Replay after theft by malware | Yes, unless bound to the device | Yes, unless sender-constrained |
| Binding options | Device Bound Session Credentials (DBSC) | DPoP (RFC 9449), mutual TLS certificate binding |
The patterns in between
RFC 10017 describes three architectures for browser-based OAuth applications, ordered from most to least secure. They are the real choice most SPA teams face.
| Pattern | What the browser holds | Where tokens live | Main residual risk |
|---|---|---|---|
| Backend for Frontend (BFF) | An HttpOnly session cookie only | On a server-side confidential client that proxies API calls | Session riding through XSS while the page is open; CSRF must be handled |
| Token-mediating backend | A cookie for the backend plus access tokens fetched on demand | Refresh tokens on the backend; access tokens in the browser | Stolen access tokens replayed until expiry |
| Browser-based OAuth client | Access and refresh tokens | In the browser, as a public client using authorisation code with PKCE | Exposed to all the token theft scenarios the RFC describes |
For the browser-based client, the RFC requires the authorisation server to either rotate refresh tokens on each use or sender-constrain them, and to cap their lifetime or expire unused ones. For the BFF and token-mediating patterns it requires the Secure and HttpOnly cookie flags, recommends SameSite=Strict, path /, no Domain attribute and the __Host-Http- name prefix, and makes CSRF protection mandatory, recommending a custom request header combined with CORS. See the OAuth 2.1, GNAP and FAPI Guide for the wider protocol background.
When to use each
- Server-rendered web application: use a server-side session cookie. There is no reason to expose a token to the browser.
- SPA calling your own API on the same site: use a BFF or plain session cookie. This is the default recommendation for new builds.
- SPA calling several third-party or cross-domain APIs: prefer a BFF that proxies those calls. Choose a token-mediating backend only if proxying is impractical, and keep access tokens short-lived and in memory.
- Pure static SPA with no backend you control: a browser-based OAuth client with PKCE is acceptable for lower-risk data, with refresh token rotation, DPoP where the authorisation server supports it and a strict Content Security Policy.
- Native mobile and desktop apps: bearer tokens obtained through the system browser with PKCE, stored in the platform keystore.
- Service-to-service, CLI tools and AI agents: bearer tokens, ideally short-lived and sender-constrained, never browser cookies. See API Keys vs OAuth Tokens.
Small teams get the most from a framework's built-in cookie session with secure defaults; building token handling into a SPA adds code that has to be right. Large organisations with many SPAs and APIs often standardise on a shared BFF or gateway component so each team does not reimplement token storage, refresh and CSRF handling.
Hardening each model
Session cookies
- Attributes: OWASP's recommended form is
Set-Cookie: __Host-SessionID=<value>; Secure; HttpOnly; SameSite=Strict; Path=/. The__Host-prefix forces Secure, no Domain and Path=/. See the Cookie Security Guide. - CSRF: treat SameSite as defence in depth, not the only control; use synchroniser tokens or a required custom header checked server-side. See the CSRF Prevention Guide.
- Timeouts: enforce idle and absolute timeouts on the server. OWASP cites 2 to 5 minutes idle for high-value applications, 15 to 30 minutes for low-risk ones, and gives 4 to 8 hours as an absolute timeout example for a full working day.
- Logout: invalidate the session on the server, including any rotated IDs; deleting the cookie alone does not stop a stolen copy. Consider
Clear-Site-Dataat logout. - Fixation: regenerate the ID at login and on privilege change. See the Session Fixation Guide.
Bearer tokens
- Transport: RFC 6750 requires TLS and prefers the Authorization header; tokens must not appear in page URLs or query strings, where logs and history keep them.
- Lifetime: RFC 6750 recommends tokens valid for one hour or less, especially for browser clients. See the Token Lifetime Policy Design Guide.
- Storage: OWASP advises never placing session IDs, access tokens, JWTs or refresh tokens in localStorage or sessionStorage, because any script on the origin can read them. If a token must be in the browser, keep it in memory. See the Browser Storage Security Guide.
- Audience and scope: validate
aud, issuer and expiry on every API, and issue the narrowest scope for each API. - Sender-constraining: DPoP binds tokens to a client key pair using a signed proof in the
DPoPheader. RFC 9449 states that with a non-extractable private key, exfiltrated tokens alone are unusable, but also warns that script running in the page can still mint fresh proofs while the client is online, so it complements rather than replaces XSS prevention.
Migrating from tokens in the browser to a BFF
- Map the current flow: list every place the SPA reads, stores, refreshes or forwards a token, including third-party SDKs and analytics scripts that can see storage.
- Stand up the BFF as the OAuth confidential client: it runs the authorisation code flow, holds access and refresh tokens server-side and issues an HttpOnly session cookie to the browser.
- Route API calls through the BFF one API at a time, attaching the access token server-side, while the old direct path stays available behind a feature flag.
- Add CSRF protection before the switch: a required custom header checked by the BFF, plus SameSite on the session cookie.
- Test in a pilot ring: check refresh behaviour, logout, multi-tab use and session expiry; watch error rates and support tickets.
- Remove browser token storage and the old client registration once traffic has moved; roll back by re-enabling the flag if error rates rise.
Security trade-offs that decide the choice
- XSS changes shape, not severity: with HttpOnly cookies an attacker who gets script execution can still make requests as the user while the tab is open. With tokens in script reach, the same bug lets them take the credential away and use it later from their own infrastructure. The second is harder to detect and lasts longer. Prevent XSS either way: see the Cross-Site Scripting (XSS) Prevention Guide.
- Revocation versus scale: server sessions revoke instantly but need shared state; self-contained JWTs scale without lookups but stay valid until expiry. Short access token lifetimes plus revocable refresh tokens is the usual compromise. See the Global Token Revocation Guide.
- Theft after MFA: neither model survives malware that copies the credential from the endpoint. Binding (DBSC for cookies, DPoP or mTLS for tokens) and anomaly detection are the counters. See Device-Bound Session Credentials (DBSC).
Worked scenarios
Session cookie scenario: a server-rendered banking portal. The portal sets a __Host- prefixed session cookie with Secure, HttpOnly and SameSite=Lax, rotates the session identifier at login, and keeps state on the server. An infostealer copies the cookie from a customer's browser; because the session lives on the server, the fraud team can kill it instantly, and the re-use from a new device and network is flagged by session anomaly rules mapped to ATT&CK T1539 and T1550.004.
Bearer token scenario: a public API used by partner systems. Partners call the API with short-lived access tokens obtained through client credentials. A partner leaks one token in a log file. Because tokens last minutes, are audience-restricted and are sender-constrained with DPoP (RFC 9449), the copied token cannot be replayed from another client, and the partner's client credential is rotated without affecting other partners.
Detection and measurement
- Log: session creation, regeneration and termination; token issuance, refresh, refresh token reuse and revocation; DPoP proof failures; and the IP, ASN and user agent behind each session or token.
- Alert: the same session or token used from two distant locations or user agents; refresh token reuse after rotation; tokens presented after logout; ATT&CK T1539 Steal Web Session Cookie and T1550.004 Web Session Cookie patterns; T1528 Steal Application Access Token for OAuth tokens. See How to detect use of a stolen OAuth refresh token.
| Metric | What it shows |
|---|---|
| Share of browser apps where no access or refresh token reaches JavaScript | Adoption of BFF or cookie sessions |
| Median and maximum access token lifetime issued to browser clients | Replay window after theft |
| Share of session cookies with Secure, HttpOnly and SameSite set | Baseline cookie hygiene |
| Time from logout or account disable to all sessions and tokens rejected | Real revocation speed |
| Refresh token reuse detections per month | Whether rotation is catching theft |
Common failure patterns
- Tokens in localStorage "because cookies are old": one XSS bug or malicious dependency exports every user's token.
- Cookies without CSRF protection after moving back from tokens: teams adopt a BFF and forget the browser now sends a credential automatically.
- Long-lived bearer tokens to avoid refresh complexity: a token valid for days turns any leak, including one in a HAR file or log, into a lasting breach. See HAR Files and Session Token Leakage.
- Logout that only clears the client: the stolen copy of the cookie or JWT keeps working.
- Treating MFA as session protection: in the CircleCI breach, malware on an engineer's laptop stole a valid SSO session that had already passed two-factor authentication, and the attacker used it to reach production systems.
- Tokens in URLs: access tokens passed in query strings end up in proxy logs, analytics and browser history, which RFC 6750 warns against.
Framework mapping
| Framework | Reference | How it applies |
|---|---|---|
| NIST SP 800-53 Rev 5 | SC-23 Session Authenticity; SC-23(1) Invalidate Session Identifiers at Logout | Protecting session identifiers and ending them on logout, for cookies and tokens alike |
| NIST SP 800-53 Rev 5 | AC-12 Session Termination | Idle and absolute timeouts, server-side termination |
| NIST CSF 2.0 | PR.AA-03 users, services and hardware are authenticated | Session and token validation on every request |
| MITRE ATT&CK v20 | T1539 Steal Web Session Cookie; T1550.004 Web Session Cookie; T1528 Steal Application Access Token | Theft and replay of cookies and tokens |
| IETF | RFC 6750; RFC 9449; RFC 10017 (BCP 212) | Bearer token usage, DPoP binding, browser app architecture |
Practitioner checklist
- Inventory browser apps and record where each stores its credential: HttpOnly cookie, memory, localStorage or sessionStorage.
- Move new SPAs to a BFF or same-site cookie session by default; require an exception for tokens in the browser.
- Set
__Host-prefixed session cookies with Secure, HttpOnly, SameSite and Path=/. - Add server-side CSRF protection to every cookie-authenticated, state-changing endpoint.
- Cap browser access token lifetime at one hour or less and rotate refresh tokens on every use.
- Enable DPoP or mTLS binding where the authorisation server and APIs support it.
- Make logout and account disable revoke server sessions and refresh tokens, and test it.
- Deploy a strict Content Security Policy and dependency review to reduce XSS exposure.
- Alert on session or token reuse from a new location, device or user agent.
Standards and references
- IETF: RFC 10017 OAuth 2.0 for Browser-Based Applications
- IETF: RFC 6750 The OAuth 2.0 Authorization Framework: Bearer Token Usage
- IETF: RFC 9449 OAuth 2.0 Demonstrating Proof of Possession (DPoP)
- OWASP: Session Management Cheat Sheet
- OWASP: Cross-Site Request Forgery Prevention Cheat Sheet
- MITRE ATT&CK: T1539 Steal Web Session Cookie
Related NHI Mgmt Group resources: Token and Session Security Guide · Session Hijacking and Cookie Theft Guide · Browser Storage Security Guide · Session Token Theft Response Playbook