How to Architect a Frontend Authentication System
A step-by-step guide on how to design a secure, complete frontend authentication system covering token storage, protected routes, session management, and token refresh flows.
Choose the Right Token Storage Strategy
Never store JWT access tokens in localStorage. LocalStorage is accessible to any JavaScript on the page, making tokens vulnerable to XSS attacks. Instead, store access tokens in memory as a JavaScript variable. They are lost on refresh which is acceptable if you use refresh tokens. Store refresh tokens in HttpOnly cookies that JavaScript cannot read at all. Only the browser automatically includes them in requests to your backend, protecting them from XSS entirely.
Implement the Token Refresh Flow
Access tokens should have short lifetimes of fifteen to sixty minutes. When an access token expires, the application needs to get a new one silently without forcing the user to log in again. Implement a silent token refresh by sending a request to the refresh token endpoint. The browser automatically includes the HttpOnly cookie containing the refresh token. The server validates it and returns a new access token. Store the new access token in memory and retry the original failed request.
Design the Auth Context Architecture
Create a centralized Auth Context that wraps the entire application. The context stores the current access token, the decoded user object from the token, authentication status, and the login and logout functions. On initial load, attempt a silent token refresh to restore the session from a valid refresh token cookie. Show a loading screen during this check to prevent a flash of the unauthenticated state before the session is restored.
Implement Protected Route Components
Create a ProtectedRoute component that reads authentication status from the Auth Context. If the user is authenticated, render the requested page. If not authenticated, redirect to the login page using React Router's Navigate component while preserving the original URL in a state parameter. After successful login, redirect back to the originally requested URL so users land on the page they were trying to access rather than a generic dashboard.
Implement Role-Based Access Control on Routes
Different user roles should access different parts of the application. Extend the ProtectedRoute component to accept a required roles array. After confirming authentication, check if the user's role from the Auth Context is included in the required roles. If not authorized, redirect to a 403 Forbidden page rather than the login page. Never rely solely on frontend route protection. The backend must independently verify authorization on every API request.
Configure an API Client with Auth Interceptors
Manually attaching the Authorization header to every API call is error-prone and verbose. Configure a central Axios instance or a fetch wrapper with a request interceptor that automatically reads the current access token from the Auth Context and attaches it to every outgoing request. Add a response interceptor that detects 401 Unauthorized responses, triggers the silent token refresh flow, updates the access token, and automatically retries the original failed request.
Handle Concurrent Requests During Token Refresh
When the access token expires, multiple in-flight requests may simultaneously receive 401 responses and each try to refresh the token independently. This causes multiple refresh requests, which can invalidate the refresh token if your backend uses rotating refresh tokens. Implement a token refresh lock using a Promise. The first 401 triggers the refresh and stores the Promise. Subsequent 401 responses queue on that same Promise rather than triggering new refreshes. All queued requests resume when the single refresh completes.
Implement Secure Logout
Logout must be thorough. Clear the in-memory access token. Send a request to the server's logout endpoint so the server invalidates the refresh token in its database and clears the HttpOnly cookie. Clear any user data from global state. If the user has the app open in multiple tabs, broadcast the logout event to all other tabs using the BroadcastChannel API or the storage event so they all redirect to the login page simultaneously rather than continuing in a partially authenticated state.
Ready to master this completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.

