[OAuth] Bài 2: Khóa chặt Access Token với DPoP và Refresh Token Rotation
![[OAuth] Bài 2: Khóa chặt Access Token với DPoP và Refresh Token Rotation](/_next/image?url=https%3A%2F%2Fnllgiwbecqskpipvcvpt.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fblog-images%2F0.33546581234837536.png&w=3840&q=75)
Chào anh em.
Ở Bài 1, PKCE đã khóa chặt khâu xin cấp Token. Nhưng nếu bản thân cái Access Token bị lộ sau khi đã nằm trong tay Client thì sao?
Đa số các hệ thống hiện nay dùng Bearer Token (Token mang khuyết danh). Bản chất của nó giống như tờ tiền mặt: Bất kỳ ai cầm được đều có thể tiêu. Hacker trộm được token từ log, bộ nhớ trình duyệt, hoặc chặn bắt gói tin (Man-in-the-Middle) là có thể gọi API hợp lệ. Kịch bản này gọi là Token Replay Attack.
Để giải quyết, RFC 9700 ép buộc các kiến trúc hệ thống hiện đại phải chuyển sang cơ chế Sender-Constrained Tokens (Token ràng buộc người gửi) hoặc chí ít phải áp dụng Refresh Token Rotation.
1. DPoP (Demonstrating Proof-of-Possession): Đóng dấu "chính chủ" lên Token
Thay vì phát "tiền mặt", DPoP (RFC 9449) biến Access Token thành một chiếc "thẻ ngân hàng" yêu cầu chữ ký sống của đúng chủ thẻ tại mỗi lần giao dịch.
Cơ chế hoạt động:
Tạo khóa: Client tự sinh ra một cặp Public/Private Key (thường là RSA hoặc Elliptic Curve) trực tiếp trên thiết bị và giấu kín Private Key.
Xin Token: Khi gọi lên Authorization Server để xin Token, Client gửi kèm Public Key (được đóng gói trong một header gọi là DPoP proof). Auth Server sinh ra Access Token và nhúng trực tiếp mã hash của Public Key đó vào bên trong token. Lúc này, Token đã bị "trói" chặt với Client.
Gọi API: Khi gọi lên Resource Server (Backend API), Client dùng Private Key ký lên cấu trúc của request hiện tại (bao gồm URL, HTTP Method, Timestamp) tạo thành một chuỗi DPoP Header đính kèm cùng Access Token.
Xác thực: Resource Server bóc token ra, lấy Public Key hash, và kiểm tra xem chữ ký ở DPoP Header có khớp với khóa và nội dung request hay không.
Kết quả: Kẻ gian ăn cắp được Access Token nhưng không có Private Key nằm ở thiết bị của Client. Không tạo được chữ ký DPoP hợp lệ -> Request bị Backend từ chối ngay lập tức.
(Lưu ý: Một kỹ thuật khác là mTLS - RFC 8705 cũng giải quyết bài toán tương tự bằng chứng chỉ x509 ở tầng Network, thường dùng cho giao tiếp server-to-server).
2. Refresh Token Rotation: Cạm bẫy phát hiện rò rỉ
Với các ứng dụng SPA (React, Vue) chạy trên trình duyệt, việc giữ Private Key an toàn tuyệt đối cho DPoP là một thách thức lớn. Nếu kiến trúc chưa thể hỗ trợ DPoP hoặc mTLS, RFC 9700 yêu cầu bắt buộc phải bật cơ chế Refresh Token Rotation (Xoay vòng Token).
Cơ chế hoạt động: Thay vì cấp một Refresh Token sống dai dẳng hàng tháng trời, hệ thống sẽ xoay vòng nó liên tục mỗi khi sử dụng.
Khách hàng dùng Refresh Token (A) để xin Access Token mới.
Auth Server trả về Access Token mới kèm theo một Refresh Token hoàn toàn mới (B).
Refresh Token cũ (A) lập tức bị đánh dấu là đã sử dụng.
Cách cơ chế này bẫy kẻ gian (Breach Detection):
Giả sử Attacker trộm được Refresh Token (A). Hắn gọi lên Auth Server và đổi trót lọt ra Access Token của hắn + Refresh Token (B). (Lúc này Attacker tưởng đã lọt lưới).
Nhưng khi User thật (đang cầm bản gốc của Refresh Token A) gọi API đổi token.
Auth Server phát hiện: "Mã A này đã được dùng rồi! Tại sao lại có yêu cầu đổi tiếp?".
Hệ thống ngay lập tức kích hoạt quy trình bảo mật: Thu hồi và vô hiệu hóa TOÀN BỘ chuỗi token (cả B, cả Access Token) thuộc về phiên đăng nhập đó.
Kết quả là Attacker lập tức mất quyền truy cập. User thật bị văng ra, yêu cầu login lại từ đầu (đánh đổi trải nghiệm để bảo vệ tài khoản một cách cực đoan).
![[OAuth] Bài 0: Vì sao những kiến thức bảo mật bạn biết có thể đã lỗi thời?](/_next/image?url=https%3A%2F%2Fnllgiwbecqskpipvcvpt.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fblog-images%2F0.9385837373059448.webp&w=3840&q=75)
![[OAuth] Bài 1: Khai tử Implicit Grant & Kỷ nguyên bắt buộc của PKCE](/_next/image?url=https%3A%2F%2Fnllgiwbecqskpipvcvpt.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fblog-images%2F0.14058804452103146.png&w=3840&q=75)