API Token Storage: Best Practices Report

Introduction

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.

Industry Best Practices

Storing API tokens securely is critical to prevent unauthorized access. Here are the recommended best practices:

Storage Options Comparison

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

Server-Side Cookie Management

The server sets the token in a cookie via the Set-Cookie header, leveraging browser security features.

Backend Example (Python/Flask)

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

Frontend Example (JavaScript)

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);
}

Advantages

Disadvantages

Client-Side Cookie Management

The client receives the token in the response body and manages it in a cookie, manually sending it in requests.

Backend Example (Python/Flask)

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

Frontend Example (JavaScript)

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');
}

Advantages

Disadvantages

Security Considerations

Recommendations

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.