Access token ngắn hạn (5–15 phút) gửi kèm Authorization: Bearer; refresh token dài hạn chỉ dùng để xin access mới.
Lưu ở đâu: refresh token trong cookie httpOnly + Secure + SameSite (JS không đọc được → chống XSS đánh cắp); access token giữ in-memory (biến JS). Tránh localStorage cho cả hai vì XSS đọc được.
Xoay vòng token (rotation): mỗi lần gọi /refresh cấp refresh mới + vô hiệu cái cũ. Nếu một refresh cũ (đã dùng) bị dùng lại → coi là bị đánh cắp, thu hồi cả "family" token của phiên đó.
Thu hồi: server lưu refresh dạng hash để có thể thu hồi; logout = xoá bản ghi + xoá cookie.
Xử lý 401: khi access hết hạn (401) → gọi /refresh rồi retry request; gộp nhiều 401 đồng thời thành một lần refresh (single-flight) để tránh race.
let refreshing = null
async function authFetch(req) {
const retry = req.clone() // clone trước khi body bị tiêu thụ
let res = await fetch(req)
if (res.status === 401) {
refreshing ??= fetch("/refresh", { credentials: "include" })
await refreshing.finally(() => { refreshing = null })
retry.headers.set("Authorization", `Bearer ${getAccessToken()}`)
res = await fetch(retry) // retry một lần với access token mới
}
return res
}