Frontend Authentication Architecture: JWT vs HttpOnly Session Cookies
Secure frontend authentication stores sensitive session/refresh tokens in HttpOnly, Secure, SameSite=Strict cookies to protect against JavaScript-based token theft (XSS), while using short-lived in-memory access tokens for API authorization.
Why it matters in interviews
Visual & Interactive Explanation
Silent Token Refresh with HttpOnly Cookies Flow
Sequence & Data Exchange Flow
1. API call needed: read in-memory access token
2. Token expired (>15 mins)
3. POST /api/auth/refresh (Browser sends HttpOnly Cookie automatically)
4. Validate refresh token & rotate token in DB
5. Set-Cookie new HttpOnly refresh token + Return new access token in JSON body
6. Save new access token in memory & resume original API call
Code Examples & Implementation
let inMemoryAccessToken: string | null = null;
let isRefreshing = false;
let failedQueue: Array<{ resolve: (token: string) => void; reject: (err: any) => void }> = [];
const processQueue = (error: any, token: string | null = null) => {
failedQueue.forEach((prom) => {
if (error) prom.reject(error);
else prom.resolve(token!);
});
failedQueue = [];
};
export async function authenticatedFetch(url: string, options: RequestInit = {}): Promise<Response> {
// 1. If we have a token, attach Authorization header
const headers = new Headers(options.headers);
if (inMemoryAccessToken) {
headers.set('Authorization', `Bearer ${inMemoryAccessToken}`);
}
let response = await fetch(url, { ...options, headers, credentials: 'include' });
// 2. If 401 Unauthorized, trigger silent token refresh
if (response.status === 401) {
if (!isRefreshing) {
isRefreshing = true;
try {
const refreshRes = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include', // Sends HttpOnly cookie
});
if (!refreshRes.ok) throw new Error('Session expired');
const data = await refreshRes.json();
inMemoryAccessToken = data.accessToken;
processQueue(null, data.accessToken);
} catch (err) {
processQueue(err, null);
window.location.href = '/login';
throw err;
} finally {
isRefreshing = false;
}
}
// Queue concurrent requests while token is refreshing
return new Promise((resolve, reject) => {
failedQueue.push({
resolve: (newToken) => {
headers.set('Authorization', `Bearer ${newToken}`);
resolve(fetch(url, { ...options, headers, credentials: 'include' }));
},
reject,
});
});
}
return response;
}Interview-Ready Answers
Never store sensitive JWT tokens in localStorage because any XSS vulnerability can read them via JavaScript. Use short-lived in-memory access tokens paired with long-lived HttpOnly, Secure, SameSite cookies for refresh tokens.
Official Documentation & Specifications
Follow-up Questions & Deep Dives
Common Mistakes & Anti-Patterns
- Storing authentication JWTs in localStorage or sessionStorage for convenience.
- Setting SameSite=None on authentication cookies without the Secure flag (modern browsers reject this).
- Failing to implement request queueing during silent token refreshes, causing dozens of parallel 401 retry storms.
Real-World Architectural Scenario
A security audit reports a Medium vulnerability because JWTs are in localStorage. How do you transition an enterprise SPA to secure cookie auth without breaking existing mobile apps?
Rate Your Readiness
Rate how comfortably you can explain this in an interview to update your global readiness gauge: