JWT Token Storage — Secure Client-Side Storage for Access and Refresh Tokens
In this tutorial, you will learn about JWT Token Storage. We cover key concepts, practical examples, and best practices to help you master this topic.
JWT token storage on the client must balance accessibility (tokens must be available for API calls) and security (tokens must be protected from theft).
What You'll Learn
The pros and cons of each storage location, how to choose the right strategy, and security considerations for web and mobile applications.
Why It Matters
Token storage is the most common JWT vulnerability. Storing tokens in localStorage makes them accessible to any JavaScript on the page (XSS vulnerability). Choosing the wrong strategy compromises all other security measures.
Real-World Use
Auth0 recommends storing tokens in memory for SPAs. Firebase stores ID tokens in memory and refreshes via httpOnly cookies. GitHub uses httpOnly cookies for its web session.
flowchart TD
A["Token Storage"] --> B["Web App"]
A --> C["Mobile App"]
B --> D["httpOnly Cookie\nBest for security"]
B --> E["In-Memory\nGood for SPAs"]
B --> F["localStorage\nAvoid if possible"]
C --> G["Keychain (iOS)\nor Keystore (Android)"]
style A fill:#dbeafe,stroke:#2563eb
style D fill:#dcfce7,stroke:#16a34a
style E fill:#fef3c7,stroke:#d97706
style F fill:#fecaca,stroke:#dc2626
style G fill:#dcfce7,stroke:#16a34a
Storage Comparison
| Storage | XSS Protection | Persistence | Complexity |
|---|---|---|---|
| httpOnly Cookie | Excellent (JS cannot access) | Browser session or expiry | Low |
| In-Memory (variable) | Good (XSS access limited) | Lost on page refresh | Medium |
| localStorage | None (JS can read) | Permanent | Low |
| sessionStorage | None (JS can read) | Tab session | Low |
| Web Worker | Good (isolated) | Lost on worker restart | High |
Code Example: httpOnly Cookie Strategy
from flask import Flask, make_response, jsonify, request
import jwt, datetime
app = Flask(__name__)
SECRET = "your-secret"
@app.route("/api/auth/login", methods=["POST"])
def login():
data = request.get_json()
# Validate credentials (simplified)
if data.get("password") != "correct":
return jsonify({"error": "Invalid credentials"}), 401
token = jwt.encode({
"sub": data["username"],
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
}, SECRET, algorithm="HS256")
# Set access token as httpOnly cookie
response = make_response(jsonify({"message": "Logged in"}))
response.set_cookie(
"access_token", token,
httponly=True, # JavaScript cannot read
secure=True, # HTTPS only
samesite="Lax", # CSRF protection
max_age=900 # 15 minutes
)
return response
@app.route("/api/profile")
def profile():
token = request.cookies.get("access_token")
if not token:
return jsonify({"error": "Not authenticated"}), 401
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
return jsonify({"user": payload["sub"]})
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
Code Example: In-Memory Storage for SPAs
// Memory-based token storage — survives page navigation but not refresh
const tokenStore = (() => {
let accessToken = null;
let refreshToken = null;
return {
setTokens(access, refresh) {
accessToken = access;
refreshToken = refresh;
},
getAccessToken() {
return accessToken;
},
getRefreshToken() {
return refreshToken;
},
clear() {
accessToken = null;
refreshToken = null;
}
};
})();
// Usage
async function login() {
const response = await fetch('/api/auth/login', { method: 'POST', ... });
const data = await response.json();
tokenStore.setTokens(data.access_token, data.refresh_token);
}
async function apiCall() {
let token = tokenStore.getAccessToken();
// Refresh if needed (check expiry)
if (isTokenExpired(token) && tokenStore.getRefreshToken()) {
const refreshResponse = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'X-Refresh-Token': tokenStore.getRefreshToken() }
});
const data = await refreshResponse.json();
tokenStore.setTokens(data.access_token, data.refresh_token);
token = data.access_token;
}
return fetch('/api/data', {
headers: { 'Authorization': `Bearer ${token}` }
});
}
Common Mistakes
1. Storing Tokens in localStorage
localStorage is accessible to any JavaScript running on the page. A single XSS vulnerability exposes all tokens.
2. Storing Refresh Tokens with Access Tokens
If access tokens are in memory but refresh tokens are in localStorage, the refresh token is vulnerable even though the access token is not.
3. Not Clearing Tokens on Logout
When the user logs out, tokens in memory or variables must be cleared. httpOnly cookies can be cleared server-side.
4. Using sessionStorage for Refresh Tokens
sessionStorage is cleared when the tab closes. This logs the user out unexpectedly. Use httpOnly cookies for persistence.
5. Storing Sensitive Data in JWTs When Using localStorage
If you must use localStorage, at least ensure the JWT contains no sensitive data. But prefer httpOnly cookies.
Practice Questions
- Why is localStorage considered insecure for token storage?
- What is the most secure token storage strategy for web apps?
- How does an httpOnly cookie protect against XSS?
- Why do SPAs typically store tokens in memory?
- How should mobile apps store tokens?
Answers:
- Any JavaScript running on the page (including XSS-injected scripts) can read localStorage via
localStorage.getItem(). - httpOnly cookies with Secure and SameSite flags. JavaScript cannot access them, and they are sent automatically with requests.
- The httpOnly flag prevents JavaScript from reading the cookie via
document.cookie. Even if XSS executes, the attacker cannot access the token. - In-memory tokens are not accessible to JavaScript from other origins and are lost on page refresh (defense against persistent XSS).
- iOS Keychain or Android Keystore — encrypted, device-specific, and application-isolated storage.
Challenge: Implement a token storage strategy for an SPA that uses httpOnly cookies for access tokens and in-memory storage for refresh tokens. Include silent refresh on page load using a dedicated refresh endpoint.
FAQ
Mini Project
Implement a Flask + JavaScript SPA with httpOnly cookie-based token storage. The login sets the token as an httpOnly Secure SameSite cookie. All API calls use cookies automatically. The SPA never touches the token directly.
What's Next
Now learn about JWT Blacklisting — how to revoke JWTs when short expiry is not enough.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro