Skip to content
GeeksSmith
advanced 9 min read

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

Storing authentication JWTs in localStorage or sessionStorage exposes user sessions to immediate extraction via XSS vulnerabilities or rogue third-party npm dependencies. Authentication security is one of the most critical topics in frontend interview rounds.

Visual & Interactive Explanation

Silent Token Refresh with HttpOnly Cookies Flow

Sequence & Data Exchange Flow

Browser Client
Memory Token Store
Next.js API Route
Auth Server
#1
Browser ClientMemory Token Store
compute

1. API call needed: read in-memory access token

#2
Memory Token StoreBrowser Client
compute

2. Token expired (>15 mins)

#3
Browser ClientNext.js API Route
request

3. POST /api/auth/refresh (Browser sends HttpOnly Cookie automatically)

#4
Next.js API RouteAuth Server
request

4. Validate refresh token & rotate token in DB

#5
Auth ServerBrowser Client
response

5. Set-Cookie new HttpOnly refresh token + Return new access token in JSON body

#6
Browser ClientMemory Token Store
compute

6. Save new access token in memory & resume original API call

Code Examples & Implementation

Silent Token Refresh Interceptor (Fetch Wrapper)
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:

Rate your confidence: