Loading…
Loading…
Access token stored in React state (in-memory), refresh token in HttpOnly cookie. Provides XSS protection for the refresh token while allowing client-side token access.
User submits credentials. Server validates and generates both tokens.
Refresh token in HttpOnly cookie, access token in response body.
Access token stored in React state or module variable (in-memory).
Access token in Authorization header, refresh cookie auto-sent.
# .env.local
NEXT_PUBLIC_API_URL=http://localhost:3001
ACCESS_TOKEN_SECRET=your-access-token-secret-min-32-chars
REFRESH_TOKEN_SECRET=your-refresh-token-secret-min-32-chars
ACCESS_TOKEN_EXPIRY=15m
REFRESH_TOKEN_EXPIRY=7d// src/lib/api-client.ts
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
let inMemoryToken: string | null = null;
export const setAccessToken = (token: string | null) => { inMemoryToken = token; };
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001',
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
});
apiClient.interceptors.request.use(
(config) => {
if (inMemoryToken) config.headers.Authorization = 'Bearer ' + inMemoryToken;
return config;
},
(error) => Promise.reject(error)
);
let isRefreshing = false;
let failedQueue: Array<{ resolve: (token: string) => void; reject: (error: any) => void; }> = [];
const processQueue = (error: any, token: string | null) => {
failedQueue.forEach((prom) => { error ? prom.reject(error) : prom.resolve(token!); });
failedQueue = [];
};
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); })
.then((token) => { originalRequest.headers.Authorization = 'Bearer ' + token; return apiClient(originalRequest); });
}
originalRequest._retry = true; isRefreshing = true;
try {
const res = await axios.post(process.env.NEXT_PUBLIC_API_URL + '/auth/refresh', {}, { withCredentials: true });
const { accessToken } = res.data;
inMemoryToken = accessToken;
processQueue(null, accessToken);
originalRequest.headers.Authorization = 'Bearer ' + accessToken;
return apiClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
inMemoryToken = null;
window.location.href = '/auth/login';
return Promise.reject(refreshError);
} finally { isRefreshing = false; }
}
return Promise.reject(error);
}
);