Skip to content

Storing JWTs in the Browser

Where you store a JWT in the browser is a security decision with real trade-offs. Understanding those trade-offs helps you make an informed choice.

localStorage.setItem('token', jwt)
const token = localStorage.getItem('token')
localStorage.removeItem('token')

Pros: Persistent across page reloads, simple API, survives browser close/reopen.

Cons: Accessible by any JavaScript on the page — vulnerable to XSS (Cross-Site Scripting) attacks. If a malicious script runs on your page (via a compromised dependency, ad injection, or DOM-based XSS), it can steal the token.

// Store in React state or Context
const [token, setToken] = useState<string | null>(null)

Pros: Not accessible to any script outside your bundle — immune to XSS.

Cons: Lost on page reload — user is logged out whenever they refresh. Fine for some apps, annoying for others.

The most secure option:

  • The server sets the JWT in an HttpOnly cookie
  • Browsers send the cookie automatically with every request to the same domain
  • JavaScript cannot read HttpOnly cookies — immune to XSS

Cons: Requires CORS credential configuration. Harder to set up for cross-domain APIs (your API on Railway, frontend on GitHub Pages). Requires CSRF protection.

Bulletin stores the token in localStorage — and acknowledges the trade-off clearly to students.

For a learning project, localStorage is fine:

  • Bulletin’s codebase doesn’t use any third-party scripts that could execute XSS
  • The users and data aren’t sensitive
  • The simplicity outweighs the security cost for a tutorial context

For a real production app handling sensitive data, httpOnly cookies are the right choice. The React Native, Electron, or same-domain setup makes them practical.

src/constants.ts
export const TOKEN_KEY = 'bulletin_token'
export const USER_KEY = 'bulletin_user'

Using constants prevents typos when reading/writing storage across the app.

Store the minimum needed:

// On login/register success:
localStorage.setItem(TOKEN_KEY, token)
localStorage.setItem(USER_KEY, JSON.stringify({ userId, username }))

The JWT has a userId and username in its payload — but verifying the JWT client-side is pointless (the server verifies it). Store a simple user object separately for quick access in the UI.

Think through the security model:

  1. Write a scenario where localStorage token storage leads to a security breach.
  2. Why doesn’t storing in React state (memory) have this problem?
  3. Why is httpOnly cookie storage immune to XSS?
  4. For Bulletin, is localStorage acceptable? What would need to change to make it production-grade?
  • localStorage: persistent but accessible to JavaScript — XSS risk.
  • Memory (React state): secure but lost on reload.
  • httpOnly cookies: most secure (JavaScript can’t read), but complex for cross-domain APIs.
  • Bulletin uses localStorage for simplicity — acceptable for learning, document the trade-off in production.