Photo by JuniperPhoton on Unsplash
Nếu bạn từng làm việc với Spring Boot và Spring Security thì hẳn bạn không còn lạ gì với JWT, nhưng bạn đã từng thử Refresh Token Rotation và WebFlux chưa? Mình thì rồi và phải nói là gặp tương đối nhiều khó khăn, tài liệu không nhiều, ít implement hay example để dựa theo. Nên mình hi vọng một bài viết nho nhỏ về cách mình implement có thể giúp các bạn đến sau setup thử với stack khá thú vị này này.
Để hiểu rõ kĩ thuật này, bài toán mà nó giải quyết cũng như ý tưởng thì mình nghĩ nên bắt đầu bằng việc nói sơ qua JWT [ 1 ] và Refresh Token, bạn nào nắm được rồi có thể phần 2 nha.
Đây là một phương thức để Authorize và Authenticate nhưng khác với những phương pháp truyền thống như Session Cookie là Stateful, JWT được thiết kế để Stateless. Để thực hiện điều này, JWT sử dụng Token, đây là một mã khóa hai chiều (có thể giải mã được) của một chuỗi JSON. Token này được duy nhất Frontend lưu và được gửi kèm trong mọi request (trừ White list) để Backend Authentication.
Ok, ok mình nghĩ các bạn cũng đã hiểu ý tưởng rồi, nhưng các bạn có thử đặt câu hỏi tại sao ta lại cần phải rắc rối như vậy không nhỉ?
Trong cách làm này, ta dơn giản là cho request login, signup vào White list của Middleware và không cần kiểm tra Access Token. Sau khi login thành công, ta sẽ mã hóa một chuỗi có cấu trúc và gửi cho Frontend để lưu vào browser (ví dụ local storage). Từ thời điểm này, mỗi khi Frontend gửi một request nào tới Backend đều sẽ kèm theo Token và khi Backend nhận được sẽ thử parse Token này ra nếu parse ra được dạng chính xác thì sẽ xác nhận request này.
Vấn đề
Hướng giải quyết
Với cách giải quyết ở trên ta sẽ gặp vấn đề là Access Token tạo ra sẽ bị expired, để bảo mật ta thường cho thời gian này khá nhỏ 1 ngày hoặc 1 tháng khiến người dùng sẽ bị văng khỏi ứng dụng và phải đăng nhập lại,. Như vậy, sẽ làm giảm đáng kể trải nghiệm của người dùng. Từ đó đẫn tới khái niệm Refresh Token là một loại token được generate ngẫu nhiên và được lưu vào DB, mục đích của loại token này là để Frontend dùng với mục đích tạo ra một Access Token mới.
Như vậy Refresh Token sẽ tương tự như Session Cookie, đó là lý do ta ko dùng mã 2 chiều trong trường hợp này. Cũng vì lý do này nên Best Practice là khi dùng Refresh Token là thay vì response trong body như Access Token thì sẽ được truyền trên header tương tự Cookie với httponly
.
Khi khách hàng login thành công, Backend không chỉ gửi Access Token mà còn cả Refresh Token. Sau đó Frontend sẽ chủ động lưu cả 2 Token này vào browser nhưng mỗi khi gọi request thì Frontend sẽ chỉ gửi kèm Access Token tương tự như Implementation 1. Khi Frontend gửi request và Backend kiểm tra thấy là Access Token đã hết hạn thì sẽ trả lỗi để Frontend nhận biết. Khi này Frontend sẽ dùng Refresh Token để gửi request (nằm trong White list) yêu cầu cấp một cái Access Token mới.
Vấn đề
Vấn đề trên gồm 2 ý và ta sẽ giải quyết từng ý một, Refresh Token là long live và kẻ đột nhập dùng để tạo Access Token. Với ý một, ta sẽ giảm tuổi thọ của Refresh Token bằng cách khi Frontend gửi Refresh Token nhằm lấy Access Token mới thì Backend sẽ tạo ra một Refresh Token mới kèm với Access Token mới để trả lại. Đồng thời Refresh Token cũ sẽ bị vô hiệu hóa (không nhất thiết phải xóa trong DB). Như vậy lifetime của Refresh Token sẽ đồng thời hết khi Access Token hết hạn.
Với hướng giải quyết này ta sẽ chặn được kẻ đột nhập sử dụng lại Refresh Token. Nhưng khi kẻ đột nhập sử dụng Refresh Token trước khi Access Token hết hạn và nhận một Refresh Token mới, trong khi đó khách hàng đợi Access Token hết hạn mới gọi và sẽ không thành công do Refresh Token đã bị vô hiệu hóa.
Để giải quyết cả kịch bản này ta vẫn sẽ vô hiệu hóa Refresh Token nhưng để chặn kẻ đột nhập ta sẽ đồng thời vô hiệu hóa Refresh Token của kẻ đột nhập sau khi được tạo mới. Nghĩ là ta sẽ lưu lịch sử của Refresh Token bắt đầu thì khi được tạo khi đăng nhập và cả những Refresh Token sau được sinh ra khi gọi API generate Access Token mới và Refresh Token mới. Khi một phần tử trong lịch sử này khác cái mới nhất bị gọi lại thì ta sẽ vô hiệu hóa hoàn toàn tất cả Refresh Token trong chuỗi này.
Vấn đề
Vấn đề trong cách Implementation 4 tương đối khó giải quyết vì có giả thuyết tương đối mạnh, với cả khả năng read và write, kẻ đột nhập có thể xem là ngang hàng với người dùng và không thể phát hiện được. Vì vậy những ông lớn trong ngành về Microservice như Netflix đã sử dụng cơ chế Backend for Frontend (BFF). Nghĩa là thay vì chỉ có Frontend và Backend thì sẽ có thêm một một Backend nhỏ nằm giữa với nhiệm vụ duy nhất là lưu Refresh Token và Access Token đồng thời nhận request của Frontend, thêm Token và gửi tới Backend.
Trong phần một của bài viết này mình đã trình bày phần lý thuyết của Refresh Token Rotation và lý do vì sao ta cần phải làm như vậy, trong phần kế mình sẽ đi implement cơ chế này trong Spring Boot và WebFlux.