DEV Community

Rvn
Rvn

Posted on

🔐 Secure Auth Token Handling in the Frontend: When Your Client Needs the Token

💬 “Do you have any tips for auth flows where the client legitimately needs the token for APIs?”
This is one of the most common — and dangerous — pain points for modern web devs.

If you’ve ever built a frontend app that needs to call an API, you’ve probably asked:

👉 Where do I store the token safely?
👉 How do I protect against XSS or CSRF?
👉 What if the client actually needs the token to work?

Let's walk through why this is tricky, and how to design a secure flow that balances usability and security — even when the client needs access to the token.


🧠 Why It’s Dangerous: The Frontend is an Unsafe Place

Your browser app (React, Vue, Svelte, etc.) runs in a sandboxed environment… but it’s still exposed to the DOM, scripts, and user extensions. That makes it vulnerable to:

  • 🧨 XSS (Cross-Site Scripting): Malicious scripts can steal tokens if they’re stored in localStorage or sessionStorage.
  • 🪝 CSRF (Cross-Site Request Forgery): If you're using cookies incorrectly, attackers can trick users into making unwanted requests.

So yes, you should be very careful when storing or exposing tokens on the frontend. But sometimes — you have to.


✅ Legit Use Cases Where Client Needs the Token

There are valid reasons why your frontend app may need direct access to a token:

  • Calling 3rd-party APIs (e.g., Stripe, Firebase, or custom backend services)
  • Using GraphQL clients like Apollo that inject tokens into headers
  • Managing mobile apps or Electron apps that mimic frontend environments
  • Single-page apps (SPAs) where API access is deeply integrated into UI logic

So, let’s build a secure plan.


🧰 5 Secure Practices for Token-Based Auth on the Frontend

1. 🛑 Avoid localStorage for Sensitive Tokens

It's easy. It's tempting. But it's not safe.

// ❌ Not secure
localStorage.setItem("token", yourJWT);
Enter fullscreen mode Exit fullscreen mode

Why not? Because if any script on your site is compromised — even via a small XSS vulnerability — your token is instantly leaked.

Use localStorage only if:

  • It’s a public-access token
  • The token has limited access scope
  • You’ve fully audited your code for XSS protection

Otherwise...


2. ✅ Use In-Memory Storage for Access Tokens

Store the token in memory (e.g., in a React context or state variable). This way, it's never written to disk and disappears when the page is refreshed.

// ✅ Safer: only in memory
let accessToken = null;

function login() {
  accessToken = fetchToken();
}

function callAPI() {
  fetch("/api/data", {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Not vulnerable to XSS
  • Not persistent after refresh (good for security)

Cons:

  • You lose the token on refresh. This is where a refresh token helps.

3. 🔁 Use HttpOnly Cookies for Refresh Tokens

Refresh tokens are designed to get new access tokens silently. Store the refresh token in an HttpOnly, Secure, SameSite=Strict cookie.

Set-Cookie: refresh_token=abc.def.ghi; HttpOnly; Secure; SameSite=Strict
Enter fullscreen mode Exit fullscreen mode
  • HttpOnly = JS can’t touch it ✅
  • Secure = Only sent over HTTPS ✅
  • SameSite = Prevents CSRF ✅

When the access token expires, the client can request a new one via an API route that automatically uses the refresh token.


4. 📦 Use a Secure Auth Pattern: "Split Token Storage"

Here’s a hybrid approach many modern apps use:

Token Type Where to Store Lifetime Usage
Access Token Memory only Short (5–15 min) Sent in headers, manually
Refresh Token HttpOnly Cookie Long (7–30 days) Used by backend to re-issue

This pattern ensures:

  • ⚔️ No tokens are exposed in JavaScript
  • 🔁 Session can be refreshed silently
  • 🔐 Access token is never stored on disk

5. 🛡️ Add Extra Layers of Protection

Don’t stop at token handling. Secure your entire stack:

  • CSP (Content Security Policy): Prevent inline scripts
  • XSS Protection: Sanitize all inputs & templates
  • Token Rotation: Invalidate and replace old tokens regularly
  • Audit Headers: Set X-Content-Type-Options, X-Frame-Options, etc.
  • Scopes & Claims: Use role-based access with JWT claims to limit token power

🧪 Real-World Flow Example (React + Express)

Here’s a simplified version:

Login Flow:

  1. User logs in via /api/login
  2. Backend:
  • Sends access token → response JSON (short-lived)
  • Sends refresh token → HttpOnly cookie (long-lived)

    1. Frontend:
  • Stores access token in memory

Accessing API:

  • React app sends Authorization: Bearer <access_token> in headers

Token Expired?

  • API returns 401
  • Frontend silently requests /api/refresh (uses HttpOnly refresh cookie)
  • Backend sends a new access token
  • App retries the original request

🧠 TL;DR – Best Practices

  • ❌ Don’t store sensitive tokens in localStorage
  • ✅ Keep access tokens in memory only
  • 🍪 Use HttpOnly cookies for refresh tokens
  • ♻️ Rotate and expire tokens properly
  • 🧱 Harden your frontend with CSP and XSS protections

💬 Final Thoughts

Yes — sometimes the client needs the token. But with the right architecture, you can still stay safe, fast, and scalable.

If you’re building an SPA or mobile frontend, designing a robust auth system is one of the most valuable skills you can develop as a dev.

Top comments (2)

Collapse
 
nevodavid profile image
Nevo David

pretty cool seeing all this broken down without fluff - always makes me think about that whole tradeoff between safety and just getting things done. you think people will ever really stop using localstorage for sensitive stuff or is it always gonna sneak back in?

Collapse
 
vincenttommi profile image
Vincent Tommi

great