Chia làm ba phần: nơi lưu token, thời điểm refresh, và xử lý nhiều request cùng hết hạn.
Nơi lưu. Access token nên sống ngắn (5–15 phút) và giữ trong bộ nhớ (biến/module state). Refresh token dài hạn nên nằm ở cookie HttpOnly + Secure + SameSite để JavaScript không đọc được — nếu trang dính XSS thì token trong localStorage bị lấy trọn.
Thời điểm refresh. Hai cách: chủ động (đặt timer trước khi hết hạn) hoặc bị động (gặp 401 thì refresh rồi gọi lại request cũ). Thực tế hay dùng bị động qua interceptor.
Vấn đề khó nhất là nhiều request cùng nhận 401: nếu mỗi cái tự gọi refresh sẽ có N lần refresh song song, refresh token xoay vòng (rotation) sẽ bị vô hiệu. Cách xử lý là gom về một promise refresh duy nhất, các request còn lại chờ promise đó rồi retry:
let refreshing = null
async function authFetch(url, options = {}) {
let res = await fetch(url, { ...options, credentials: 'include' })
if (res.status !== 401) return res
refreshing ??= fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' })
.finally(() => { refreshing = null })
const refreshed = await refreshing
if (!refreshed.ok) { redirectToLogin(); return refreshed }
return fetch(url, { ...options, credentials: 'include' }) // retry once
}Retry đúng một lần để tránh vòng lặp vô hạn; refresh hỏng thì xoá state và chuyển về trang đăng nhập.
Quan trọng: mọi kiểm tra ở client chỉ để cải thiện trải nghiệm — server vẫn phải xác thực lại từng request.