Câu chuyện JWT và Revoke Token

Posted by MinhHungTrinh on 2021-01-17
Estimated Reading Time 12 Minutes
Words 2k In Total
Viewed Times

Đây là bài mình viết cũng đã trình bày trên blog kaopiz.kipalog của công ty.

Nói về JWT chắc hẳn ae làm web service đều clear về khái niệm này và áp dụng thuần thục nó với authorized. Ở đây mình không nói chi tiết về nó nữa mà nói về bài toán dưới đây.

Bài toán Revoke Token khi làm việc với JWT, xử lý như thế nào để tốt, security hơn.

Bài viết có tham khảo vài solutions từ thread trao đổi từ các anh em trong bài post của anh Việt Trần trong JsLand group cũng như kiến thức cá nhân từng làm việc với vài dự án kiểu như thế này.

Chi tiết hơn về logic cái gọi là revoke token: Hệ thống có nghiệp vụ được yêu cầu quản lý tất cả các phiên đăng nhập (các đầu thiết bị), ở đây hệ thống đang nói đến sử dụng JWT. Nghĩa là:

  • Hệ thống phải giới hạn được số lượng tối đa thiết bị/phiên đăng nhập (ở đây là số lượng JWT) mà 1 user được phép sử dụng.
  • User có thể revoke all token hoặc revoke 1 token cụ thể. Token bị revoke sẽ không thể sử dụng được nữa.

Nghe có vẻ thấy nghiệp vụ này giống facebook chúng ta đang dùng nhỉ :)))

Bản chất JWT là stateless, tức là nó không phụ thuộc vào server. Vì vậy việc muốn revoke 1 JWT của 1 user là không thể, trừ khi chúng ta tự phải customize nó.

Hình dưới đây là sequence diagram làm việc của JWT.

main

Vấn đề là nhiều bạn thảo luận ở bước 3, việc lưu token ở server là sai. Một số bạn khác cho rằng nếu không lưu lại token chúng ta sẽ không thể thực hiện revoke token được.
Sau đây mình xin liệt kê vài cách xử lý mà có thể chúng ta đã từng gặp và đánh giá về nó:

1. Lưu cả token lên luôn database => so bad.

Để giải quyết bài toán ở đầu bài, rất nhiều các engineer chọn lưu lại JWT của người dùng để tiện quản lý, phục vụ cá nghiệp vụ trên. Nếu thực hiện revoke thì sẽ xoá token đó trên database. Các request lên server sẽ kiểm tra xem có token đó trên database không. Nếu không thì reject.

Nếu ta lưu cả token, đây là vấn đề mà rất nhiều hệ thống có thể đang làm, là một sai lầm rất nghiêm trọng. Và tất nhiên việc lưu token trên database cũng làm mất bản chất stateless của JWT. :)

Giải pháp trên sẽ là một sai lầm về bảo mật, nó giống như việc chúng ta đang lưu password (plain text) của user. Vì giữ full token nghĩa là ta đang có full các permission có liên quan đến token đó.

Giả sử nếu vì một lý do nào đó bạn bị leak mất thông tin database (Do tấn công mất database, do nội bộ từ dev, .v.v.v). Thì coi như kẻ xấu đã có full quyền của user rồi.

Tất nhiên có nhiều bạn nói hệ thống mà để hacker tấn công được database thì các thông tin server cũng lộ hết rồi. Điều này thì đúng với các hệ thống xây dựng monolith rồi. Nhưng với hệ thống hướng microservice thì chưa hẳn vì các thành phần độc lập. Nên có thể là lộ database của 1 node (component) nào đó của hệ thống thôi chứ chưa chắc là lộ hết tất cả các node. Vì thế với mỗi thành phần của hệ thống hãy cố gắng bảo mật, hạn chế. Việc không lưu full ít nhất cũng giúp hạn chế hacker phần nào khi lộ database.

2. Lưu lên database, nhưng không lưu full token, lưu chữ ký số.

Đây là ý tưởng được đưa ra nhằm cải thiện hơn về bảo mật và không gian lưu trữ cho solution 1.

Nói qua ý tưởng này cần nhắc lại các thành phần của JWT có 3 phần: header, payload và chữ ký số.

Trong đó phần payload là public, tức là ai cũng xem được, nhưng không thể sửa (một cách hợp lệ). Vì phần chữ kí số quyết định tính chất hợp lệ của payload, chỉ có server (nơi giữ secret key) mới có thể modify/create token.

Chữ ký số là một dạng mã hoá một chiều. Chỉ có thể mã hoá, không thể giải ngược. Vì thế nó được dùng để chứng thực độ tin cậy của payload. Thực tế là hàm verify token chỉ dùng secret key để thực hiện mã hoá payload rồi so sánh lại xem có khớp với chữ kí token không thôi.

Từ đó ta thấy rằng thay vì lưu full JWT, ta chỉ cần lưu phần chữ ký số là đủ. Người nắm giữ phần này cơ bản là không thể giải ngược được payload. Việc này có 2 tác dụng: giảm lượng data cần lưu (rất đáng kể) trên database và tăng bảo mật cho đầu nghiệp vụ quản lý JWT của người dùng. Tất nhiên chỉ là tăng bảo mật thôi nhé.

3. Lưu JWT ID lên database

Một solution nữa, và mình nghĩ nó sẽ tốt hơn 2 solutions ở trên.

Mỗi token sinh ra thì sinh ra cho nó một trường duy nhất gọi là JWT ID, trường đó đc lưu ở payload dưới dạng jti: generated_id. Và server chỉ cần lưu trường jti này trên database, đánh dấu revoked hay chưa. Như vậy khi nhận được JWT request từ client là server lập tức có thể xác nhận đc JWT đc gửi lên đã revoked hay chưa.

Việc này vừa không để lộ thông tin token, vừa giảm thiểu data phải lưu. Điều cần chú ý là không được sinh 2 JWT có jti trùng nhau. Vì nếu trùng sẽ sinh ra trường hợp revoke ông này thì revoke ông kia.

4. Set expire time ngắn và lưu Redis on RAM

Bản chất các solutions trên đã làm hỏng mất cơ chế stateless của JWT rồi. Và với mỗi request chúng ta đều phải thực hiện truy vấn database để kiểm tra có tồn tại bản ghi trong đó để thoả mãn. Với một hệ thống lớn, to thì mình nghĩ nó sẽ ảnh hưởng tới performance.

Vậy mình sẽ lựa chọn giải pháp là set expire time cho JWT token với thời gian ngắn. Và nếu chúng ta cần thực hiện revoke thì hãy lưu lại refresh token của user bào black list. Lưu ý là mình sẽ chọn backlist lưu on RAM để có thể truy vấn nhanh nhất. Ở đây mình lựa chọn Redis, vì nó luôn backup về disk nên nếu có vấn đề tắt đột ngột nó cũng sẽ không mất data.

Refreshtoken thường được lưu trữ ở cookies, chúng ta thường có logic, lưu trữ sau xxx ngày thì expire chẳng hạn. Thì có thể tối ưu black list bằng cách chạy daily check xem refresh token đã expire theo logic chưa => mục đích giảm số lượng refresh trong black list.

Ta cũng có thể yên tâm là số lượng user bị revoke luôn nhỏ hơn tổng số lượng user rất nhiều :)) .

Chỉ đáng tiếc là cách làm này không quản lý được số JWT đang đăng nhập của user. Từ đó không đáp ứng được yêu cầu số 1 là giới hạn số JWT đăng nhập => kiểu như nghiệp vụ của facebook vậy.

Kết luận

Bạn nên nhớ chỉ là yêu cầu nghiệp vụ làm mình phải đưa ra các giải pháp như thế này chứ không nhất thiết lúc nào phải làm như thế này. Nếu dùng JWT thì hãy tận dụng hết sức mạnh của nó nhé. Ở trên là vài solution mình gom góp được. Và chắc chắn mình sẽ update nó thêm khi làm việc nhiều hơn với bài toán này. Mọi người có solution nào tốt hơn có thể bổ sung hộ mình dưới comment nhé.

À có 1 giải pháp khi muốn revoke all users chẳng hạn. Thay đổi cặp secret key - public key trên server (nếu muốn vô hiệu hoá toàn bộ JWT được cấp phát bởi cặp key trên - trường hợp gặp phải lỗi bị tấn công nghiêm trọng). Cái này nhanh và tiện này nhưng mà làm phải cẩn thận nhé :D


Đây là Blog cá nhân của MinhHungTrinh, nơi mình chia sẻ, lưu giữ kiến thức. Nếu các bạn có góp ý, thắc mắc thì vui lòng comment bên dưới cho mình biết nhé. Mình luôn là người lắng nghe và ham học hỏi. Các vấn đề đặc biệt hoặc tế nhị mọi người có thể gửi email tới minhhungtrinhvn@gmail.com. Cảm ơn Mọi Người đã đọc Blog của mình. Yolo!