Token Storage — Complete Secure Storage Guide
In this tutorial, you will learn about Token Storage. We cover key concepts, practical examples, and best practices to help you master this topic.
Token storage determines where and how authentication tokens are stored on the client side, directly impacting security against XSS attacks, CSRF attacks, and token theft, making it one of the most critical decisions in authentication architecture.
What You'll Learn
By the end of this lesson, you will understand the trade-offs between HTTP-only cookies, localStorage, in-memory storage, and secure enclaves, implement secure token storage for web and mobile apps, and protect tokens against common attack vectors.
Why It Matters
The most secure authentication protocol is useless if tokens are stored insecurely. Storing JWTs in localStorage makes them accessible to any XSS attack. Durga Antivirus Pro uses HTTP-only cookies for session tokens and in-memory storage for API access tokens, with refresh tokens in secure storage.
Real-World Use
A single-page application receives a JWT access token and refresh token from the auth server. Storing them in localStorage makes them vulnerable to XSS. Instead, the app stores the access token in a JavaScript variable (in-memory) and the refresh token in an HTTP-only cookie. The access token is lost on page refresh, but the refresh cookie persists and silently obtains a new token.
Token Storage Options
flowchart TB
subgraph "Storage Options"
A[HTTP-only Cookie]
B[localStorage]
C[Session Storage]
D[In-Memory]
E[Secure Enclave]
end
subgraph "Security"
F[XSS Protection]
G[CSRF Protection]
H[Persistence]
end
A -->|High| F
A -->|Low| G
B -->|Low| F
B -->|High| G
D -->|High| F
D -->|High| G
D -->|None| H
style A fill:#4CAF50,color:#fff
style B fill:#f90,color:#fff
style D fill:#2196F3,color:#fff
HTTP-Only Cookie Storage
const express = require("express");
const app = express();
app.post("/api/auth/login", (req, res) => {
const accessToken = "jwt-access-token";
const refreshToken = "jwt-refresh-token";
// Access token in memory (return in body)
// Refresh token in HTTP-only cookie
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: true,
sameSite: "strict",
path: "/api/auth",
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken, expiresIn: 900 });
});
app.post("/api/auth/refresh", (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: "No refresh token" });
}
// Verify refresh token and issue new access token
const newAccessToken = "new-jwt-token";
res.json({ accessToken: newAccessToken, expiresIn: 900 });
});
app.post("/api/auth/logout", (req, res) => {
res.clearCookie("refreshToken", { path: "/api/auth" });
res.json({ message: "Logged out" });
});
In-Memory Token Storage (SPA)
// Token storage module for SPA
class TokenManager {
constructor() {
this.accessToken = null;
this.refreshPromise = null;
}
setTokens(accessToken) {
this.accessToken = accessToken;
}
getAccessToken() {
return this.accessToken;
}
async refreshAccessToken() {
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = fetch("/api/auth/refresh", {
method: "POST",
credentials: "include",
})
.then(res => res.json())
.then(data => {
this.accessToken = data.accessToken;
this.refreshPromise = null;
return this.accessToken;
})
.catch(err => {
this.refreshPromise = null;
this.accessToken = null;
throw err;
});
return this.refreshPromise;
}
clear() {
this.accessToken = null;
this.refreshPromise = null;
}
}
// Axios interceptor that uses in-memory token
const tokenManager = new TokenManager();
axios.interceptors.request.use(config => {
const token = tokenManager.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
axios.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
try {
const newToken = await tokenManager.refreshAccessToken();
error.config.headers.Authorization = `Bearer ${newToken}`;
return axios(error.config);
} catch {
window.location.href = "/login";
}
}
return Promise.reject(error);
}
);
Storage Comparison
| Method | XSS Protection | CSRF Protection | Persistence | Use Case |
|---|---|---|---|---|
| HTTP-only Cookie | High | Low (needs CSRF token) | Per browser session | Traditional web apps |
| localStorage | Low | High | Until cleared | Legacy SPAs (avoid) |
| Session Storage | Low | High | Per tab | Short sessions |
| In-Memory | High | High | Until page refresh | SPAs with refresh |
| Secure Enclave/Keychain | High | High | Permanent | Mobile apps |
Common Mistakes
- Storing access tokens in localStorage makes them accessible to any XSS script.
- Storing refresh tokens in localStorage exposes long-lived credentials to the same XSS risk.
- Not using the
pathattribute on cookies allows the cookie to be sent to all endpoints. - Mixing cookie-based auth with CORS without proper
SameSiteandcredentialsconfiguration. - Storing tokens without checking the target origin allows open redirect token theft.
- Using the same storage mechanism for both access and refresh tokens reduces defense in depth.
Practice Questions
- Why is localStorage considered insecure for token storage?
localStorage is accessible to any JavaScript running on the same origin. A single XSS vulnerability can exfiltrate all stored tokens. HTTP-only cookies are not accessible to JavaScript and survive XSS attacks.
- How does in-memory token storage work with page refreshes?
In-memory tokens are lost on page refresh. The refresh token stored in an HTTP-only cookie allows the SPA to silently obtain a new access token on initial load, creating a seamless experience.
- What is the BFF (Backend for Frontend) pattern for token storage?
The BFF pattern moves token storage to a backend proxy. The browser never receives tokens directly. The BFF stores tokens in HTTP-only cookies and proxies API calls. This eliminates client-side token storage entirely.
- Challenge: Implement a secure token storage architecture for a SPA using in-memory access tokens, HTTP-only cookie refresh tokens, automatic refresh on 401, and BFF pattern for additional security.
FAQ
Mini Project: Token Storage Security Scanner
Build a CLI tool that scans web applications for insecure token storage patterns.
import re
import sys
class TokenStorageScanner:
PATTERNS = {
"localStorage_token": r"localStorage\.(setItem|getItem).*((access|refresh)_?token|jwt)",
"sessionStorage_token": r"sessionStorage\.(setItem|getItem).*((access|refresh)_?token|jwt)",
"cookie_without_httponly": r"document\.cookie\s*=[^;]*(token|jwt|session)[^;]*(?!.*HttpOnly)",
"token_in_url": r"\?(.*&)?(token|jwt|api_key)=[^&\s]+",
}
def scan_file(self, filepath):
with open(filepath) as f:
content = f.read()
print(f"Scanning: {filepath}")
found_issues = False
for pattern_name, pattern in self.PATTERNS.items():
matches = re.finditer(pattern, content, re.IGNORECASE)
for match in matches:
found_issues = True
line_num = content[:match.start()].count("\n") + 1
print(f" [ISSUE] Line {line_num}: {pattern_name}")
print(f" Match: {match.group()[:60]}...")
if not found_issues:
print(" No token storage issues found")
return found_issues
def scan_directory(self, path):
from pathlib import Path
issues = 0
for f in Path(path).rglob("*.js"):
if self.scan_file(f):
issues += 1
print(f"\nTotal files with issues: {issues}")
if __name__ == "__main__":
scanner = TokenStorageScanner()
scanner.scan_directory(sys.argv[1] if len(sys.argv) > 1 else ".")
What's Next
Learn about CSRF protection for web authentication security, then explore auth middleware for backend request authentication.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro