As a React developer, managing API tokens securely can be confusing. Where should they be stored after retrieval? Cookies, local storage, session storage, or in-memory? This report explores two primary approaches—server-side cookie management and client-side cookie management—and provides industry best practices, code examples, and security insights to help developers make informed decisions.
Storing API tokens securely is critical to prevent unauthorized access. Here are the recommended best practices:
HttpOnly, Secure, and SameSite attributes to protect against XSS and CSRF.Understanding the differences between storage methods is key to choosing the right one:
| Feature | Cookies | Local Storage | Session Storage |
|---|---|---|---|
| Location | Browser (sent with requests) | Browser (client-side) | Browser (client-side) |
| Persistence | Configurable (expiration) | Persistent until cleared | Cleared on tab close |
| Size Limit | ~4KB | ~5-10MB | ~5-10MB |
| Access | HTTP + JS (unless HttpOnly) | JavaScript only | JavaScript only |
| Risks | CSRF (mitigated by SameSite) | XSS | XSS |
The server sets the token in a cookie via the Set-Cookie header, leveraging browser security features.
from flask import Flask, request, jsonify, make_response
import jwt
import datetime
app = Flask(__name__)
SECRET_KEY = "your-secret-key"
@app.route('/api/login', methods=['POST'])
def login():
data = request.get_json()
if data.get('username') == "user" and data.get('password') == "pass":
token = jwt.encode(
{"username": "user", "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30)},
SECRET_KEY,
algorithm="HS256"
)
response = make_response(jsonify({"message": "Login successful"}), 200)
response.set_cookie("token", token, httponly=True, secure=True, samesite="Strict", max_age=1800)
return response
return jsonify({"message": "Invalid credentials"}), 401
@app.route('/api/protected', methods=['GET'])
def protected():
token = request.cookies.get('token')
if not token:
return jsonify({"message": "Token missing"}), 401
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return jsonify({"message": f"Welcome, {payload['username']}!"})
except:
return jsonify({"message": "Invalid token"}), 401
async function login() {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'user', password: 'pass' }),
credentials: 'include'
});
const data = await response.json();
console.log(data.message);
}
async function getProtectedData() {
const response = await fetch('/api/protected', {
method: 'GET',
credentials: 'include'
});
const data = await response.json();
console.log(data.message);
}
HttpOnly (blocks XSS).The client receives the token in the response body and manages it in a cookie, manually sending it in requests.
from flask import Flask, request, jsonify
import jwt
import datetime
app = Flask(__name__)
SECRET_KEY = "your-secret-key"
@app.route('/api/login', methods=['POST'])
def login():
data = request.get_json()
if data.get('username') == "user" and data.get('password') == "pass":
token = jwt.encode(
{"username": "user", "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30)},
SECRET_KEY,
algorithm="HS256"
)
return jsonify({"message": "Login successful", "token": token}), 200
return jsonify({"message": "Invalid credentials"}), 401
@app.route('/api/protected', methods=['GET'])
def protected():
token = request.headers.get('Authorization', '').split(' ')[1] if 'Bearer' in request.headers.get('Authorization', '') else None
if not token:
return jsonify({"message": "Token missing"}), 401
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return jsonify({"message": f"Welcome, {payload['username']}!"})
except:
return jsonify({"message": "Invalid token"}), 401
function setCookie(name, value, days) {
const expires = new Date(Date.now() + days * 86400000).toUTCString();
document.cookie = `${name}=${value}; expires=${expires}; path=/; SameSite=Strict`;
}
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
function deleteCookie(name) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
}
async function login() {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'user', password: 'pass' })
});
const data = await response.json();
if (response.ok) {
setCookie('token', data.token, 1);
console.log(data.message);
}
}
async function getProtectedData() {
const token = getCookie('token');
if (!token) {
console.log('No token found');
return;
}
const response = await fetch('/api/protected', {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
console.log(data.message);
}
function logout() {
deleteCookie('token');
console.log('Logged out');
}
HttpOnly).HttpOnly cookies or sanitize inputs and implement a CSP.SameSite cookies or CSRF tokens.Server-Side Cookies are the preferred method due to their security (HttpOnly) and simplicity. Use them when your backend can set cookies. Client-Side Cookies are a viable alternative for APIs that return tokens in JSON, but require extra care against XSS. Avoid local storage and session storage for sensitive tokens unless you have strong protections in place.