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.
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 đó.
Revoke: server lưu refresh dạng hash để có thể thu hồi; logout = xóa bản ghi + xóa 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) {
let res = await fetch(req)
if (res.status === 401) {
refreshing ??= fetch("/refresh", { credentials: "include" })
await refreshing.finally(() => { refreshing = null })
res = await fetch(req) // retry once with the new access token
}
return res
}