<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged localstorage</title>
<link>https://coderlegion.com/tag/localstorage</link>
<description>Powered by Question2Answer</description>
<item>
<title>JWT Storage: Cookies vs LocalStorage – Which Is Right for Your App?</title>
<link>https://coderlegion.com/7607/jwt-storage-cookies-vs-localstorage-which-is-right-for-your-app</link>
<description>&lt;h1&gt;JWT Storage: Cookies vs LocalStorage&lt;/h1&gt;
&lt;p&gt;Where should you store JWT tokens? This guide covers both approaches with working code examples to help you make an informed decision.&lt;/p&gt;
&lt;h2&gt;What You'll Learn&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Implement JWT authentication using HTTP-only cookies and localStorage&lt;/li&gt;
&lt;li&gt;Understand XSS and CSRF attack implications&lt;/li&gt;
&lt;li&gt;Choose the right storage method for your application&lt;/li&gt;
&lt;li&gt;Apply defense-in-depth security strategies&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Two Storage Options&lt;/h2&gt;
&lt;h3&gt;HTTP-Only Cookies&lt;/h3&gt;
&lt;p&gt;Cookies with the &lt;code&gt;httpOnly&lt;/code&gt; flag cannot be accessed by JavaScript, providing protection against XSS token theft.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key characteristics:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automatic: Browser sends tokens with requests&lt;/li&gt;
&lt;li&gt;JavaScript-proof: Cannot be stolen via XSS&lt;/li&gt;
&lt;li&gt;Configurable: &lt;code&gt;Secure&lt;/code&gt; and &lt;code&gt;SameSite&lt;/code&gt; flags for added security&lt;/li&gt;
&lt;li&gt;Vulnerable to CSRF without proper protection&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;LocalStorage&lt;/h3&gt;
&lt;p&gt;Tokens stored in localStorage require manual attachment to requests via JavaScript.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key characteristics:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manual control: You decide when to send tokens&lt;/li&gt;
&lt;li&gt;JavaScript-accessible: Easy to use but vulnerable to XSS&lt;/li&gt;
&lt;li&gt;CSRF immune: Tokens aren't sent automatically&lt;/li&gt;
&lt;li&gt;Developer-managed: You handle storage and cleanup&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Core Trade-off&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Cookies are safer from XSS but vulnerable to CSRF. LocalStorage is immune to CSRF but exposed to XSS.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Implementation: Cookies Approach&lt;/h2&gt;
&lt;h3&gt;Backend (Node.js/Express)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const express = require('express');
const jwt = require('jsonwebtoken');
const cookieParser = require('cookie-parser');

const app = express();
app.use(cookieParser());
app.use(express.json());

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';

// Login endpoint
app.post('/api/auth/login', async (req, res) =&amp;gt; {
  const { email, password } = req.body;
  const user = await validateUser(email, password);
  
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  const token = jwt.sign(
    { userId: user.id, email: user.email },
    JWT_SECRET,
    { expiresIn: '15m' }
  );
  
  res.cookie('access_token', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 15 * 60 * 1000
  });
  
  res.json({ success: true, user: { id: user.id, email: user.email } });
});

// Protected endpoint
app.get('/api/user/profile', (req, res) =&amp;gt; {
  const token = req.cookies['access_token'];
  
  if (!token) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    res.json({ user: decoded });
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Frontend (React)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;async function handleLogin(email, password) {
  const response = await fetch('http://localhost:3000/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include', // Critical for cookies
    body: JSON.stringify({ email, password })
  });
  
  const data = await response.json();
  if (response.ok) {
    window.location.href = '/dashboard';
  }
}

// Making authenticated requests
fetch('/api/user/profile', {
  credentials: 'include' // Browser automatically includes cookie
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Implementation: LocalStorage Approach&lt;/h2&gt;
&lt;h3&gt;Backend&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const express = require('express');
const jwt = require('jsonwebtoken');

const app = express();
app.use(express.json());

// Login endpoint - returns token in response body
app.post('/api/auth/login', async (req, res) =&amp;gt; {
  const { email, password } = req.body;
  const user = await validateUser(email, password);
  
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  const token = jwt.sign(
    { userId: user.id, email: user.email },
    JWT_SECRET,
    { expiresIn: '15m' }
  );
  
  res.json({ 
    success: true,
    token: token,
    user: { id: user.id, email: user.email }
  });
});

// Protected endpoint - expects Authorization header
app.get('/api/user/profile', (req, res) =&amp;gt; {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  
  const token = authHeader.substring(7);
  
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    res.json({ user: decoded });
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Frontend&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;async function handleLogin(email, password) {
  const response = await fetch('http://localhost:3000/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password })
  });
  
  const data = await response.json();
  
  if (response.ok) {
    localStorage.setItem('access_token', data.token);
    window.location.href = '/dashboard';
  }
}

// Making authenticated requests
const token = localStorage.getItem('access_token');
fetch('/api/user/profile', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Security Deep Dive&lt;/h2&gt;
&lt;h3&gt;XSS Attacks&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;LocalStorage:&lt;/strong&gt; If XSS occurs, attackers can trivially steal tokens with &lt;code&gt;localStorage.getItem('access_token')&lt;/code&gt; and use them from anywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HTTP-Only Cookies:&lt;/strong&gt; JavaScript cannot read the token. Attackers can make requests during XSS but cannot steal the token for later use.&lt;/p&gt;
&lt;h3&gt;CSRF Attacks&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Cookies:&lt;/strong&gt; Browsers automatically send cookies with requests, even from malicious sites. Use &lt;code&gt;SameSite&lt;/code&gt; cookies and CSRF tokens for protection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LocalStorage:&lt;/strong&gt; Immune to CSRF because tokens must be manually attached via JavaScript, which cannot access cross-origin localStorage.&lt;/p&gt;
&lt;h2&gt;Making the Decision&lt;/h2&gt;
&lt;h3&gt;Choose HTTP-Only Cookies if:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Building financial, healthcare, or high-security applications&lt;/li&gt;
&lt;li&gt;Frontend and backend share a domain&lt;/li&gt;
&lt;li&gt;Using server-side rendering (Next.js, Remix)&lt;/li&gt;
&lt;li&gt;Security is top priority&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Choose LocalStorage if:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Building a modern SPA (React, Vue, Angular)&lt;/li&gt;
&lt;li&gt;Cross-origin setup with CORS complexity&lt;/li&gt;
&lt;li&gt;Building mobile or hybrid apps&lt;/li&gt;
&lt;li&gt;Using GraphQL or custom protocols&lt;/li&gt;
&lt;li&gt;Have strong XSS prevention (CSP, security audits)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Hybrid Approach (Best Security)&lt;/h3&gt;
&lt;p&gt;Store short-lived access tokens in memory (5-15 min) and long-lived refresh tokens in HTTP-only cookies. This combines the benefits of both approaches.&lt;/p&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;h3&gt;Universal (Do These Always)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Always use HTTPS in production&lt;/li&gt;
&lt;li&gt;Keep tokens short-lived (15-30 minutes)&lt;/li&gt;
&lt;li&gt;Implement token rotation&lt;/li&gt;
&lt;li&gt;Build proper logout (clear client + invalidate server)&lt;/li&gt;
&lt;li&gt;Log authentication events and monitor patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;XSS Prevention&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Implement Content Security Policy (CSP)&lt;/li&gt;
&lt;li&gt;Sanitize user input with libraries like DOMPurify&lt;/li&gt;
&lt;li&gt;Avoid dangerous DOM manipulation (&lt;code&gt;innerHTML&lt;/code&gt;, &lt;code&gt;eval&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Keep dependencies updated (&lt;code&gt;npm audit&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CSRF Prevention (For Cookies)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;SameSite&lt;/code&gt; cookie attribute (&lt;code&gt;'strict'&lt;/code&gt; or &lt;code&gt;'lax'&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Implement CSRF tokens for sensitive operations&lt;/li&gt;
&lt;li&gt;Validate request origins&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Troubleshooting&lt;/h2&gt;
&lt;h3&gt;Cookies not sent cross-origin&lt;/h3&gt;
&lt;p&gt;Backend: Set CORS with &lt;code&gt;credentials: true&lt;/code&gt;. Frontend: Use &lt;code&gt;credentials: 'include'&lt;/code&gt; in fetch.&lt;/p&gt;
&lt;h3&gt;SameSite=None not working&lt;/h3&gt;
&lt;p&gt;Requires &lt;code&gt;Secure&lt;/code&gt; flag (HTTPS only).&lt;/p&gt;
&lt;h3&gt;Tokens not syncing across tabs (localStorage)&lt;/h3&gt;
&lt;p&gt;Listen to &lt;code&gt;storage&lt;/code&gt; events to detect changes in other tabs.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;HTTP-only cookies&lt;/strong&gt; protect against XSS token theft but require CSRF protection. &lt;strong&gt;LocalStorage&lt;/strong&gt; is immune to CSRF but vulnerable to XSS. The &lt;strong&gt;hybrid approach&lt;/strong&gt; offers the best security.&lt;/p&gt;
&lt;p&gt;For high-security apps, use cookies or hybrid. For modern SPAs with strong XSS prevention, localStorage is acceptable. Remember: security is a process, not a checkbox.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;For a more detailed version of this guide with additional examples, troubleshooting scenarios, and in-depth explanations, visit: &lt;a rel=&quot;nofollow&quot; href=&quot;https://stackinsight.dev/blog/jwt-storage-cookies-vs-localstorage-which-is-right-for-your-app/&quot;&gt;https://stackinsight.dev/blog/jwt-storage-cookies-vs-localstorage-which-is-right-for-your-app/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>App Security</category>
<guid isPermaLink="true">https://coderlegion.com/7607/jwt-storage-cookies-vs-localstorage-which-is-right-for-your-app</guid>
<pubDate>Sat, 22 Nov 2025 05:19:56 +0000</pubDate>
</item>
</channel>
</rss>