JWT Authentication Flow and Silent Token Refresh0%

JWT Authentication Flow and Silent Token Refresh

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

JWT Authentication Flow and Silent Token Refresh

JSON Web Tokens (JWT) are the industry standard for stateless authentication in modern web applications. An enterprise authentication architecture requires a two-token system: short-lived Access Tokens (held in memory) and long-lived Refresh Tokens (stored in httpOnly, SameSite=Strict cookies) with automated silent token rotation via Axios or fetch interceptors.


1. The Two-Token Architecture

Storing access tokens in localStorage or sessionStorage exposes user credentials to Cross-Site Scripting (XSS) theft. Storing tokens exclusively in plain cookies exposes them to Cross-Site Request Forgery (CSRF).

Output
Enterprise Dual-Token Architecture:
┌────────────────────────┐ ┌────────────────────────┐
│ Client (React App) │ │ Auth Backend (Node/DJ) │
└──────────┬─────────────┘ └───────────┬────────────┘
│ 1. POST /api/auth/login │
├─────────────────────────────────────────────►│
│ 2. Returns: AccessToken (in JSON body) │
│ Sets: httpOnly RefreshToken cookie │
│◄─────────────────────────────────────────────┤
│ │
│ 3. Authenticated API call with Bearer Token │
├─────────────────────────────────────────────►│
│ │
│ 4. 401 Unauthorized (AccessToken expired) │
│◄─────────────────────────────────────────────┤
│ │
│ 5. POST /api/auth/refresh (cookie attached) │
├─────────────────────────────────────────────►│
│ 6. Returns new AccessToken │
│◄─────────────────────────────────────────────┤
│ │
│ 7. Replays original failed API request │
├─────────────────────────────────────────────►│

2. Axios Silent Refresh Interceptor

An enterprise Axios interceptor detects 401 Unauthorized responses, queues pending outgoing requests, executes a silent refresh, and retries the original request seamlessly without logging the user out:

TypeScript
// api/client.ts
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios";
 
export const apiClient = axios.create({
baseURL: "https://api.enterprise.com/v1",
withCredentials: true, // Crucial: automatically includes httpOnly refresh cookies
});
 
// In-memory access token storage (isolated from XSS access to localStorage)
let memoryAccessToken: string | null = null;
 
export const setAccessToken = (token: string | null) => {
memoryAccessToken = token;
};
 
// Request Interceptor: Injects Bearer token into headers
apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
if (memoryAccessToken && config.headers) {
config.headers.Authorization = `Bearer ${memoryAccessToken}`;
}
return config;
});
 
// Response Interceptor: Silent Token Refresh Queue
let isRefreshing = false;
let failedQueue: Array<{
resolve: (value?: unknown) => void;
reject: (reason?: any) => void;
}> = [];
 
const processQueue = (error: AxiosError | null, token: string | null = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
 
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
 
// Detect 401 and avoid infinite loops
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// Another request is already refreshing; queue this request
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
})
.catch((err) => Promise.reject(err));
}
 
originalRequest._retry = true;
isRefreshing = true;
 
try {
// Silent refresh request (cookie sent automatically via withCredentials)
const refreshResponse = await axios.post<{ accessToken: string }>(
"https://api.enterprise.com/v1/auth/refresh",
{},
{ withCredentials: true }
);
 
const newAccessToken = refreshResponse.data.accessToken;
setAccessToken(newAccessToken);
 
processQueue(null, newAccessToken);
 
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError as AxiosError, null);
setAccessToken(null);
// Force redirect to login page on refresh token invalidation
window.location.href = "/login?expired=true";
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
 
return Promise.reject(error);
}
);

3. Auth Context Provider

Wrap your application in an AuthProvider that initiates a silent refresh on initial application mount:

TSX
import React, { createContext, useContext, useEffect, useState } from "react";
import { apiClient, setAccessToken } from "./client";
 
interface AuthContextValue {
isAuthenticated: boolean;
isLoading: boolean;
login: (token: string) => void;
logout: () => Promise<void>;
}
 
const AuthContext = createContext<AuthContextValue | null>(null);
 
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
 
useEffect(() => {
// Initial mount: check if refresh cookie exists by performing silent refresh
apiClient
.post<{ accessToken: string }>("/auth/refresh")
.then((res) => {
setAccessToken(res.data.accessToken);
setIsAuthenticated(true);
})
.catch(() => {
setIsAuthenticated(false);
})
.finally(() => {
setIsLoading(false);
});
}, []);
 
const login = (token: string) => {
setAccessToken(token);
setIsAuthenticated(true);
};
 
const logout = async () => {
await apiClient.post("/auth/logout");
setAccessToken(null);
setIsAuthenticated(false);
};
 
return (
<AuthContext.Provider value={{ isAuthenticated, isLoading, login, logout }}>
{!isLoading && children}
</AuthContext.Provider>
);
}
 
export const useAuth = () => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
};

Interactive Quiz

Practice Quiz

Question 1
Why is storing JWT Access Tokens in localStorage considered an enterprise security vulnerability?
Question 2
Why should Refresh Tokens be stored in httpOnly, SameSite=Strict cookies?
Question 3
What is the function of the failedQueue array in the Axios response interceptor?
Question 4
What does withCredentials: true configure in Axios?
Question 5
What should happen when the /auth/refresh endpoint itself returns a 401 or 403 error?
Next Lesson

OAuth2 and Social Login Integration

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext