Skip to content

Token Storage Strategies — Memory, httpOnly Cookies, and Secure Storage Patterns

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Token Storage Strategies. We cover key concepts, practical examples, and best practices to help you master this topic.

Secure token storage prevents credential theft: in-memory variables offer the best XSS protection, httpOnly cookies prevent JavaScript access, and localStorage should be avoided for sensitive tokens.

What You'll Learn

Token storage options for web, mobile, and server clients, security properties of each storage mechanism, XSS and CSRF implications, and platform-specific best practices.

Why It Matters

A perfectly implemented auth system is worthless if tokens are stored insecurely on the client. Token theft via XSS or CSRF is one of the most common API security breaches.

Real-World Use

Auth0 recommends in-memory storage for SPAs with refresh tokens in httpOnly cookies. Google APIs use short-lived access tokens in memory. Durga Antivirus Pro stores partner tokens in encrypted browser storage with automatic expiry.

flowchart TD
    A["Token Storage Decision"] --> B["Browser SPA?"]
    B -->|"Yes"| C{"Risk tolerance?"}
    C -->|"High security"| D["In-memory variable"]
    C -->|"Balanced"| E["httpOnly Secure\nSameSite cookie"]
    C -->|"Low / Internal"| F["SessionStorage"]
    B -->|"No — Mobile"| G["Secure Enclave\nKeychain / Keystore"]
    B -->|"No — Server"| H["Environment variables\nOr secrets manager"]
    D --> I["Lost on refresh\nRequires refresh cookie"]
    E --> J["CSRF protection\nJS cannot read"]
    style D fill:#dcfce7,stroke:#16a34a
    style E fill:#dbeafe,stroke:#2563eb
    style F fill:#fecaca,stroke:#dc2626

Code Example: In-Memory Token Storage in SPA

// Token store — tokens live only in JS memory
const tokenStore = (function() {
  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;
    },
    hasTokens() {
      return accessToken !== null;
    }
  };
})();

// Usage — fetch with automatic token injection
async function apiFetch(url, options = {}) {
  const token = tokenStore.getAccessToken();
  const headers = { ...options.headers };

  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }

  const resp = await fetch(url, { ...options, headers });

  if (resp.status === 401 && tokenStore.getRefreshToken()) {
    // Attempt refresh
    const refreshed = await refreshTokens();
    if (refreshed) {
      return apiFetch(url, options);
    }
    tokenStore.clear();
    window.location.href = '/login';
  }

  return resp;
}
from flask import Flask, request, jsonify, make_response
import jwt, datetime, os, secrets

app = Flask(__name__)
SECRET = os.environ.get("JWT_SECRET", "dev-secret")

@app.route("/api/auth/login", methods=["POST"])
def login():
    data = request.get_json()
    if data.get("password") != "correct":
        return jsonify({"error": "Invalid"}), 401

    access_token = jwt.encode({
        "sub": data.get("username"),
        "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
    }, SECRET, algorithm="HS256")

    refresh_token = secrets.token_urlsafe(32)

    resp = make_response(jsonify({
        "message": "Login successful",
        "expires_in": 900
    }))

    # Set httpOnly cookie for refresh token
    resp.set_cookie(
        "refresh_token",
        refresh_token,
        httponly=True,
        secure=True,          # HTTPS only
        samesite="Strict",    # CSRF protection
        max_age=7 * 24 * 3600, # 7 days
        path="/api/auth"
    )

    return resp

@app.route("/api/auth/refresh", methods=["POST"])
def refresh():
    refresh_token = request.cookies.get("refresh_token")
    if not refresh_token:
        return jsonify({"error": "Refresh token required"}), 401

    # Validate refresh token against store
    if not validate_refresh_token(refresh_token):
        return jsonify({"error": "Invalid refresh token"}), 401

    new_access = jwt.encode({
        "sub": get_user_from_refresh(refresh_token),
        "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
    }, SECRET, algorithm="HS256")

    return jsonify({"access_token": new_access, "expires_in": 900})

Code Example: Secure Storage for Mobile (React Native)

import * as SecureStore from 'expo-secure-store';

const TOKEN_KEYS = {
  ACCESS: 'auth_access_token',
  REFRESH: 'auth_refresh_token'
};

export async function storeTokens(access, refresh) {
  await Promise.all([
    SecureStore.setItemAsync(TOKEN_KEYS.ACCESS, access, {
      keychainService: 'dodatech-api',
      keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY
    }),
    SecureStore.setItemAsync(TOKEN_KEYS.REFRESH, refresh, {
      keychainService: 'dodatech-api',
      keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY
    })
  ]);
}

export async function getAccessToken() {
  return SecureStore.getItemAsync(TOKEN_KEYS.ACCESS);
}

export async function clearTokens() {
  await Promise.all([
    SecureStore.deleteItemAsync(TOKEN_KEYS.ACCESS),
    SecureStore.deleteItemAsync(TOKEN_KEYS.REFRESH)
  ]);
}

Expected usage:

// Token stored in OS keychain — encrypted at rest
await storeTokens(accessToken, refreshToken);
const token = await getAccessToken();
// Token is accessible only by this app

Common Mistakes

1. Storing Tokens in localStorage

localStorage is accessible to any JavaScript running on the same origin. A single XSS vulnerability leaks all tokens. Use httpOnly cookies or in-memory storage instead.

Storing tokens in cookies without SameSite=Strict or CSRF tokens makes the API vulnerable to cross-site request forgery. Always set SameSite=Strict and consider CSRF tokens.

3. Mixing Storage Strategies

Some apps store access tokens in memory and refresh tokens in localStorage. The refresh token is the more valuable credential — it should have the strongest protection.

4. Storing Tokens in Redux/Vuex State

State management stores are in-memory but accessible via devtools. An attacker with browser access can read the store. Consider this for internal apps only.

5. Not Clearing Tokens on Logout

Failed to clear tokens from all storage locations (memory, cookies, secure store) can leave orphaned tokens. Implement a centralized clear function.

Practice Questions

  1. Why is localStorage considered insecure for token storage?
  2. What security properties do httpOnly cookies provide?
  3. How does SameSite=Strict prevent CSRF Attacks?
  4. Why do mobile apps use Keychain/Keystore rather than file storage?
  5. What happens to in-memory tokens when the user refreshes the page?

Answers:

  1. Any JavaScript executing on the same origin can read localStorage via localStorage.getItem(). An XSS attack exposes all stored tokens.
  2. httpOnly cookies cannot be read by JavaScript, preventing XSS-based token theft. They are automatically sent with requests to the issuing domain.
  3. SameSite=Strict prevents the browser from sending the cookie with cross-origin requests, blocking CSRF attacks from other websites.
  4. Keychain (iOS) and Keystore (Android) encrypt tokens at rest using hardware-backed encryption keys. File storage does not provide this protection.
  5. In-memory tokens are lost on page refresh. The app must use a refresh token (stored in an httpOnly cookie) to obtain new access tokens silently.

Challenge: Implement a full token storage solution for an SPA: access tokens in memory, refresh tokens in httpOnly secure SameSite cookies, with automatic silent refresh on page load.

FAQ

Should I use sessionStorage or localStorage?

Neither for sensitive tokens. sessionStorage is cleared when the tab closes, which is better than localStorage, but both are readable by JavaScript. Use httpOnly cookies or in-memory storage.

Can I encrypt tokens in localStorage?

Encryption helps but does not solve the fundamental problem: the encryption key must be in JavaScript memory, so an XSS attack can read both the key and the encrypted token.

How do mobile apps store tokens securely?

iOS uses the Keychain, Android uses EncryptedSharedPreferences or the Keystore. Both encrypt tokens using hardware-backed keys.

Does the BFF pattern solve token storage?

Yes. The Backend-for-Frontend pattern keeps tokens server-side in a session cookie. The browser never has direct access to the tokens.

What is the refresh token rotation pattern?

Each time a refresh token is used, the server issues a new refresh token and invalidates the old one. This limits the window of risk for stolen refresh tokens.

How do I handle token storage in service workers?

Service workers can intercept fetch requests and add Authorization headers from in-memory storage. They do not have access to httpOnly cookies.

Mini Project

Build a full-stack login application with an Express backend that sets httpOnly secure cookies for refresh tokens and returns access tokens in the response body. The React frontend stores access tokens in a JavaScript module variable and automatically refreshes on 401.

What's Next

Now explore JWT Access and Refresh Token Rotation for a complete token lifecycle implementation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro