Lập trình · 19/09/2026

Refresh token rotation thực chiến: Thiết kế đăng nhập an toàn cho API

Access token ngắn hạn giảm thời gian kẻ tấn công có thể sử dụng token bị lộ, nhưng refresh token lại là thông tin đăng nhập dài hạn có khả năng tạo access token mới. Nếu backend chấp nhận một refresh token nhiều lần, bản sao bị đánh cắp có thể duy trì phiên gần như âm thầm. Refresh token rotation giải quyết vấn đề bằng cách thay token sau mỗi lần sử dụng và phát hiện token cũ bị phát lại.

Refresh token rotation thực chiến: Thiết kế đăng nhập an toàn cho API

Access token ngắn hạn giảm thời gian kẻ tấn công có thể sử dụng token bị lộ, nhưng refresh token lại là thông tin đăng nhập dài hạn có khả năng tạo access token mới. Nếu backend chấp nhận một refresh token nhiều lần, bản sao bị đánh cắp có thể duy trì phiên gần như âm thầm. Refresh token rotation giải quyết vấn đề bằng cách thay token sau mỗi lần sử dụng và phát hiện token cũ bị phát lại.

Access token và refresh token có vai trò khác nhau

  • Access token: sống ngắn, gửi tới resource server, giới hạn audience và scope.
  • Refresh token: chỉ gửi tới authorization server để đổi token mới, sống lâu hơn và phải được bảo vệ chặt hơn.

JWT không bắt buộc cho refresh token. Một chuỗi ngẫu nhiên opaque thường dễ thu hồi, dễ kiểm soát và không làm lộ claim. Dù dùng định dạng nào, token phải có entropy cao, truyền qua TLS và không xuất hiện trong URL hoặc log.

Rotation hoạt động như thế nào?

  1. Đăng nhập thành công tạo access token A1 và refresh token R1.
  2. Client gửi R1 tới endpoint refresh.
  3. Server xác thực, đánh dấu R1 đã dùng và phát A2 + R2.
  4. R1 không còn hợp lệ nhưng quan hệ của nó với phiên vẫn được giữ để phát hiện reuse.
  5. Nếu R1 xuất hiện lần nữa, server thu hồi toàn bộ token family.

Server không biết request thứ hai đến từ client hợp lệ hay kẻ tấn công. Vì vậy phản ứng an toàn là hủy family và buộc đăng nhập lại.

Mô hình dữ liệu đề xuất

refresh_tokens
- id (UUID)
- family_id (UUID)
- user_id, client_id
- token_hash
- parent_id
- status: active | used | revoked
- issued_at, expires_at, used_at, revoked_at
- replaced_by_id
- scope, audience
- device_label, last_ip (optional metadata)

Chỉ lưu hash của token, tương tự password reset token. Khi nhận token, server hash bằng hàm nhất quán có khóa hoặc SHA-256 với token ngẫu nhiên đủ mạnh rồi tra cứu. Không ghi token thô vào database, APM hay audit log.

Luồng refresh phải nguyên tử

BEGIN;
SELECT * FROM refresh_tokens
WHERE token_hash = :hash
FOR UPDATE;

-- reject expired/revoked token
-- if status = used: revoke entire family and fail
-- if active: mark used, insert replacement R2

COMMIT;

Khóa hàng hoặc compare-and-swap ngăn hai request song song cùng đổi một token thành công. Unique constraint cho token_hash, transaction ngắn và index theo hash/family là các lớp bảo vệ cần thiết.

Xử lý race condition hợp lệ

Browser có thể gửi hai request API cùng lúc, cả hai thấy access token hết hạn và cùng refresh. Nếu request đầu đã rotate, request thứ hai trông giống replay. Cách tốt nhất là client dùng single-flight: chỉ một promise refresh chạy, các request khác chờ kết quả.

Một grace window phía server có thể giảm logout giả nhưng cũng mở cửa phát lại. Nếu bắt buộc dùng, cửa sổ phải rất ngắn, chỉ trả lại đúng replacement đã tạo và gắn với cùng client context; không phát thêm nhánh token mới.

Lưu token trên web và mobile

  • Web: ưu tiên refresh token trong cookie HttpOnly; Secure; SameSite, kết hợp CSRF protection phù hợp. Không lưu credential trong localStorage.
  • Mobile: dùng Keychain/Keystore và tránh backup token sang thiết bị khác.
  • BFF: browser chỉ giữ session cookie, backend-for-frontend quản lý token OAuth phía server.

Access token nên ở memory khi có thể. Cookie không tự loại bỏ XSS hay CSRF; vẫn cần CSP, encode output, kiểm tra Origin/CSRF token và giới hạn CORS.

Thời hạn và thu hồi

Dùng cả idle timeout và absolute lifetime. Mỗi lần refresh có thể kéo dài idle window nhưng không vượt quá tuổi tuyệt đối của authorization. Thu hồi family khi logout thiết bị, đổi mật khẩu, khóa tài khoản, phát hiện reuse hoặc có sự kiện rủi ro.

“Logout tất cả thiết bị” thu hồi mọi family của user; “logout thiết bị này” chỉ thu hồi family hiện tại. Access token đã phát có thể còn hiệu lực đến khi hết hạn, trừ khi resource server có introspection/denylist. Đây là lý do access token cần sống ngắn.

Response và lỗi API

POST /oauth/token
grant_type=refresh_token&refresh_token=...

200 { access_token, expires_in, refresh_token }
400 { error: "invalid_grant" }

Không tiết lộ token “hết hạn”, “đã dùng” hay “không tồn tại” cho client không tin cậy. Ghi lý do chi tiết vào security event nội bộ, kèm family ID, client ID và correlation ID nhưng không chứa token.

Checklist kiểm thử

  • Một token chỉ đổi thành công đúng một lần.
  • Hai refresh song song không tạo hai nhánh hợp lệ.
  • Dùng lại token cũ thu hồi cả family.
  • Token hết hạn, sai client, scope hoặc audience bị từ chối.
  • Logout và đổi mật khẩu thực sự chặn refresh tiếp theo.
  • Database, log, trace và analytics không chứa token thô.
  • Cookie và CSRF/CORS được kiểm thử trên browser thật.
  • Cleanup xóa bản ghi hết hạn nhưng giữ đủ dấu vết phát hiện reuse trong thời gian chính sách yêu cầu.

Khi nào nên dùng giải pháp khác?

Ứng dụng web cùng domain có thể đơn giản hơn với session cookie phía server. Hệ thống rủi ro cao có thể dùng sender-constrained token như DPoP hoặc mTLS, đôi khi kết hợp rotation. Không tự xây authorization server nếu một nhà cung cấp hoặc thư viện OAuth/OIDC trưởng thành đáp ứng yêu cầu.

Kết luận

Rotation không chỉ là phát chuỗi mới. Thiết kế đúng cần token family, trạng thái sử dụng, thao tác nguyên tử, phát hiện reuse, chiến lược revoke và client single-flight. Khi các phần này đi cùng access token ngắn hạn và lưu trữ phù hợp nền tảng, phiên đăng nhập vừa thuận tiện vừa có điểm chặn rõ ràng khi credential bị lộ.

Tài liệu tham khảo

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.