Skip to content

Token Storage — Complete Secure Storage Guide

DodaTech Updated 2026-06-28 6 min read

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
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

  1. Storing access tokens in localStorage makes them accessible to any XSS script.
  2. Storing refresh tokens in localStorage exposes long-lived credentials to the same XSS risk.
  3. Not using the path attribute on cookies allows the cookie to be sent to all endpoints.
  4. Mixing cookie-based auth with CORS without proper SameSite and credentials configuration.
  5. Storing tokens without checking the target origin allows open redirect token theft.
  6. Using the same storage mechanism for both access and refresh tokens reduces defense in depth.

Practice Questions

  1. 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.

  1. 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.

  1. 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.

  1. 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

What is the most secure way to store tokens in a browser?

HTTP-only, Secure, SameSite cookies for refresh tokens, and in-memory JavaScript variables for access tokens. The backend sets the refresh cookie; the SPA keeps the access token in memory.

How do mobile apps store tokens securely?

iOS: Keychain Services. Android: EncryptedSharedPreferences or Android Keystore. React Native: react-native-keychain. Flutter: flutter_secure_storage. Never use SharedPreferences or AsyncStorage.

What is token binding?

Token binding cryptographically binds tokens to a specific TLS connection. If the token is stolen, it cannot be used from a different TLS connection. Also known as "token pinning" or "proof-of-possession."

How does BFF pattern improve security?

The browser never receives tokens. The BFF stores tokens in HTTP-only cookies and forwards API requests. This eliminates XSS token theft entirely. The BFF runs on the same domain, avoiding CORS issues.

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