💬 “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
orsessionStorage
. - 🪝 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);
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}`,
},
});
}
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
- 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:
- User logs in via
/api/login
- Backend:
- Sends access token → response JSON (short-lived)
-
Sends refresh token →
HttpOnly
cookie (long-lived)- 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
(usesHttpOnly
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)
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?
great