Hãy ngừng sử dụng JWT
(gist.github.com/samsch)- JWT không phù hợp để duy trì trạng thái đăng nhập của người dùng, và cho mục đích này cookie session thông thường phù hợp hơn
- Đặc tả JWT giả định token có thời hạn rất ngắn, khoảng 5 phút trở xuống, trong khi session cần thời gian sống dài hơn
- Rất khó để hiện thực xác thực không trạng thái một cách an toàn, và để xử lý token an toàn thì cuối cùng vẫn cần một số nơi lưu trạng thái
- JWT chỉ chứa token session đơn giản thì kém hiệu quả và kém linh hoạt hơn session cookie thông thường, và không nên lưu thông tin xác thực trong localStorage hay sessionStorage
- Khi cần token ký có thời hạn ngắn, PASETO được thiết kế vì bảo mật là lựa chọn tốt hơn, nhưng cũng không nên dùng cho mục đích session
Tóm tắt cốt lõi
- Không nên dùng JWT để duy trì trạng thái đăng nhập của người dùng; cho mục đích đó, cookie session thông thường là công cụ tốt hơn
- JWT không được thiết kế cho mục đích này và không an toàn; để duy trì login session, cookie session tiêu chuẩn phù hợp hơn
- Ở chủ đề liên quan, không nên lưu thông tin xác thực, bao gồm cả JWT token, trong localStorage hay sessionStorage
- Có thể xem bài thuyết trình liên quan đến JWT, nhưng các chủ đề khác như bảo vệ CSRF thường chỉ được đề cập sơ lược, nên cần học riêng từ nguồn khác
- Ngay cả các trường hợp sử dụng JWT được xem là “hợp lệ” ở phần cuối video cũng có thể được xử lý dễ dàng bằng công cụ tốt hơn và an toàn hơn, cụ thể là PASETO
Vì sao nên tránh JWT
- Đặc tả JWT chỉ được thiết kế cho token có thời hạn rất ngắn, khoảng 5 phút trở xuống, trong khi session cần thời gian sống dài hơn
- Xác thực không trạng thái theo cách an toàn là điều không thể; để xử lý token an toàn thì nhất định phải có một phần trạng thái
- Nếu đã cần kho dữ liệu, thì lưu toàn bộ dữ liệu sẽ tốt hơn là chỉ xử lý một phần trạng thái token
- Vấn đề liên quan được phân tích chi tiết hơn tại http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
- Trên thực tế có ứng dụng dùng JWT theo cách này, nhưng những ứng dụng đó có lỗi thiết kế, nên không nên lặp lại sai lầm tương tự
- JWT chỉ lưu token session đơn giản thì kém hiệu quả và kém linh hoạt hơn session cookie thông thường, mà không mang lại lợi ích bổ sung nào
- Bản thân đặc tả JWT không được các chuyên gia bảo mật tin tưởng, nên cần loại nó khỏi toàn bộ các mục đích liên quan đến bảo mật và xác thực
- Đặc tả ban đầu từng cho phép tạo token giả, và có thể còn chứa những sai sót khác
- Các vấn đề của họ đặc tả JWT được phân tích sâu hơn trong JWT: The JSON Web Token standard is bad and everyone should avoid it
Phản biện
- Phản biện “Google cũng dùng JWT” không áp dụng cho browser user session
- Google không dùng JWT cho browser user session mà dùng session cookie thông thường
- JWT chỉ được dùng như phương tiện truyền Single Sign On để chuyển login session từ một server hoặc host sang session của server hoặc host khác
- Cách dùng này nằm trong phạm vi trường hợp sử dụng hợp lý của JWT
- Google có nguồn lực chuyên gia bảo mật để tạo và duy trì triển khai JWT an toàn hơn
- JWT của Google trên thực tế không giống JWT ở nơi khác
- Phản biện “không trạng thái thì tốt hơn” không phù hợp với yêu cầu xác thực an toàn
- Không có nguồn lực rất lớn thì không thể vận hành xác thực thật sự không trạng thái một cách an toàn
- Có thể tham khảo thảo luận liên quan tại Stateless is a lie
- Vấn đề “không biết cách thiết lập session” thường có thể giải quyết bằng tài liệu và cách triển khai của hầu hết các framework web server
- Công nghệ session không phải điều gì quá mới nên không thường thấy các bài viết giải thích về session
- Chỉ với tài liệu của implementation session cũng nên đủ để làm theo quá trình cấu hình
- Gần như mọi framework web server đều có implementation session; dù không bật mặc định thì thường cũng có thể bật dễ dàng
- Express và các framework Node.js khác phần nào là ngoại lệ do tính module hóa cao và tính chất đơn mục đích
- Trong Express, chỉ cần dùng middleware
express-sessioncùng store connector phù hợp với kho lưu trữ - Khuyến nghị dùng
connect-session-knexvới Postgres, MySQL, hoặc nếu có thể thì SQLite
Token ngắn hạn
- Nếu cần token ký có thời hạn ngắn cho một mục đích nào đó, có một đặc tả tốt hơn là PASETO, được thiết kế với bảo mật là ưu tiên
- Dù dùng PASETO, cũng không nên dùng cho mục đích session
Cách session hoạt động
- Nếu muốn tìm hiểu thêm về cách session hoạt động, nên xem gist của joepie91
2 bình luận
JWT là cách giảm mã hóa token và truy vấn DB, chứ không phải là khái niệm đối lập với xác thực bằng cookie. Nếu lưu JWT trong secure cookie thì rủi ro bị đánh cắp cũng giống với cách xác thực cookie kiểu legacy.
Việc quản lý danh sách hết hạn để làm cho JWT expired có lợi thế về mặt hiệu năng. Có sự khác biệt chi phí giữa việc chỉ truy vấn thông tin hết hạn trên Redis và việc truy vấn DB cho toàn bộ thành viên.
Hàng chục nghìn lần truy vấn dựa trên index trên 100.000 row thành viên (cách cookie legacy)
vs
Hàng chục nghìn lần truy vấn 50 mục trong danh sách hết hạn trên Redis (cách hết hạn JWT tức thì)
JWT đúng là có lợi điểm. Chỉ là trong môi trường quy mô nhỏ thì khác biệt ít rõ rệt hơn
Ý kiến trên Hacker News
Thiếu một manh mối cần thiết: đây là đang nói về phiên người dùng trên trình duyệt
Trong giao tiếp giữa các dịch vụ, có nhiều trường hợp JWT được dùng rất ổn
Nói thêm thì tôi đã đọc một phần bài được liên kết, ví dụ có bài như https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-ba.... Nếu JWT thực sự là một tiêu chuẩn kinh khủng đến mức mất an toàn như vậy, thì hãy công bố cách hack AssumeRoleWithWebIdentity của AWS STS, hoặc đừng công bố mà cứ triển khai thợ đào tiền mã hóa lên từng tài khoản AWS production của các công ty Fortune 500. Nếu JWT bất an đến thế thì cuối cùng thành công nhớ báo cho tôi biết /mỉa mai
Phần chữ ký và mã hóa của JWT rất phức tạp, và các thư viện JWT phổ biến cũng chỉ gần đây mới phần lớn tỉnh táo hơn, chứ trước đây thì không. Nhiều thư viện từng chấp nhận thuật toán
"none"[1], và cũng có trường hợp dùng khóa công khai như thể đó là bí mật chia sẻ, khiến kẻ tấn công có thể giả mạo token [2]. Đây chính là hệ quả do sự phức tạp mà bài viết được liên kết phê phán tạo raJWT đôi khi cũng không làm được điều bạn muốn ở phiên người dùng. Nếu không duy trì một danh sách thu hồi ở đâu đó thì không thể vô hiệu hóa nó. Nhưng nếu mỗi request đều phải đối chiếu định danh với danh sách thu hồi, thì thà dùng session ID mờ và tra cứu mỗi request còn hơn. Dĩ nhiên có thể dùng token sống ngắn và liên tục làm mới, nhưng với ứng dụng thông thường vốn đã phải duy trì trạng thái thì cũng không có nhiều lý do để làm vậy
Tuy vậy, tôi hoàn toàn đồng ý rằng trong hệ thống phân tán hoặc giao tiếp giữa máy với máy, token có chữ ký có thể rất hữu ích. Không nên nhầm lẫn hai trường hợp này
[1] https://nvd.nist.gov/vuln/detail/cve-2022-23540
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-54150
Hiện nay các thư viện lớn ở nhiều ngôn ngữ đã có mặc định lành mạnh hơn, nên tôi cho rằng bây giờ trên thực tế nó đã khá an toàn
Nếu “an toàn khi được cầm và dùng đúng cách” đồng nghĩa với thiết kế tốt, thì điều đó cũng sẽ áp dụng y hệt cho những thứ như X.509
Trong nhiều trường hợp có lựa chọn thay thế tốt hơn. Token phiên tiêu chuẩn hoặc API key được dùng rộng rãi ở hầu hết các website lớn, và gần như phù hợp hoàn hảo với đa số mục đích sử dụng
Không phải tôi muốn nói các tiêu chuẩn này hoàn toàn vô giá trị. Điểm hay nhất của chúng là một tiêu chuẩn cơ bản để trao đổi thứ gì đó mà không cần các thứ như mã hóa ASN.1, và bộ công cụ phía ASN.1 có vẻ rất mong manh và lắm lỗi
Ví dụ, tôi không biết cách khai thác SAML thế nào, nhưng tôi biết đó là một tiêu chuẩn khủng khiếp vì nó biến toàn bộ parser XML thành bề mặt tấn công. Tôi không phải nhà nghiên cứu bảo mật nên không biết cách tìm lỗ hổng trong parser XML, nhưng vẫn có thể hiểu rằng bề mặt tấn công lớn là điều tệ
JWT không an toàn ư, ý là ngay cả khi dùng cơ chế ký dựa trên RSA/khóa công khai đáng tin cậy cũng vậy sao? Không dùng bí mật chia sẻ mà vẫn thế?
Lập luận rằng JWT sống quá lâu cũng nghe kỳ lạ. Chỉ cần giới hạn thời gian sống của JWT và có mô hình làm mới với cơ quan xác thực là được. Dù dùng session dựa trên cookie thì rốt cuộc bạn vẫn đang lưu trữ gì đó ở đâu đó. Bạn có thể đặt JWT chỉ có hiệu lực 5~15 phút, và 15 phút cũng tương đương thời gian cache của nhiều hệ thống cấp quyền, bao gồm cả Entra. Token 5 phút cũng hoàn toàn dùng được trong trình duyệt nếu có hệ thống làm mới
Cuối cùng, tôi thích tách danh tính/xác thực ra khỏi ứng dụng và dịch vụ API. Bạn có thể đưa ngữ cảnh ra bên ngoài, và cách xử lý JWT trên mỗi request dễ kiểm soát hơn so với hệ thống cache/trạng thái dùng chung có thể thỉnh thoảng thất bại. Token có chữ ký có thể xác minh chữ ký đối với một tổ chức đã biết
Ngoài ra, chữ ký là hợp lệ về mặt mật mã. Chỉ cần xác minh mọi JWT mỗi lần với thời gian sống ngắn
Nhân tiện, token OIDC đều là JWT
So sánh giữa session và danh sách thu hồi JWT thì cũng có một lập luận nghiêng về danh sách thu hồi JWT. JWT có thời điểm hết hạn giới hạn, nên bạn chỉ cần duy trì danh sách thu hồi cho các token vẫn chưa hết hạn
JWT bị thu hồi có khả năng chỉ là một phần nhỏ so với các JWT hợp lệ đang lưu hành, nên mỗi request chỉ cần tra cứu một tập dữ liệu rất nhỏ
Nếu dùng session, danh sách session hợp lệ có khả năng lớn hơn danh sách thu hồi nhiều bậc độ lớn, nên chi phí tra cứu và lưu trữ do tính trạng thái sẽ cao hơn
Hơn nữa, bài viết nói JWT là không trạng thái, nhưng thường không phải vậy. Thông thường bạn không chỉ xác minh JWT mà còn lấy đối tượng danh tính tương ứng ở mỗi request, tức là chi tiết người dùng, để kiểm tra người đó còn hoạt động hay không và có quyền thực hiện thao tác đó hay không. Có thể dùng danh sách thu hồi theo từng người dùng hoặc giá trị như
minimum_issued_atđể xác minh trườngiatcủa JWT. Làm vậy cũng hỗ trợ mẫu “đăng xuất khỏi mọi thiết bị”, và hành động đó chỉ cần đặtminimum_issued_atcủa người dùng thành$NOWlà tất cả token cũ sẽ bị vô hiệu hóa. Không cần tra cứu danh sách thu hồi riêng lẻselectcó dùng chỉ mục trong cơ sở dữ liệu và trả về 0~1 hàng. Trong đa số trường hợp, đó không phải điều đáng loBài này dẫn phần lớn nội dung thuộc về câu hỏi “tại sao” sang các bài blog khác, nhưng những bài đó nhìn chung có vẻ chỉ bực bội vì “không thể vô hiệu hóa từng token JWT riêng lẻ”.
Mỗi lần tôi triển khai, hướng dẫn thông thường vẫn là kiểm tra nonce đã bị vô hiệu hóa ở đâu đó, và như vậy thì luận điểm thứ hai trong bài đó cũng được giải quyết.
Câu “ngay cả các chuyên gia bảo mật cũng không tin tưởng bản thân đặc tả JWT” có vẻ cần nhiều căn cứ hơn là chỉ một bài blog. Và bài đó phần lớn dường như đang đổ lỗi cho các cách triển khai tệ; mà với bất kỳ tiêu chuẩn nào thì vấn đề triển khai tệ cũng luôn tồn tại.
Nói chung, tôi cũng không biết mình đã kỳ vọng gì khi bấm vào một liên kết gist ngẫu nhiên.
Ngoài ra, trong trình duyệt hoàn toàn có thể dùng JWT với thời hạn ngắn hơn, rồi để agent tự gia hạn. Dùng Azure Entra hay nhiều nhà cung cấp khác thì thực tế nó hoạt động đúng như vậy. Có thể giữ JWT tương đối ngắn, khoảng 5~15 phút, và còn kiểm tra cả việc
jtiđã bị thu hồi hay chưa.JWT rất hữu ích để tách cơ quan cấp quyền truy cập ra khỏi hệ thống ứng dụng/API và tái sử dụng nó. Tức là chuyển bề mặt tấn công đi, nhưng chuyển theo cách đáng tin cậy. Trên khắp thế giới, nhiều nơi đã dùng cơ chế khóa công khai, kể cả SSH. Tôi sẽ không dùng bí mật chia sẻ hay token sống lâu, nhưng token ký bằng khóa công khai, thời hạn ngắn, đến từ nguồn đã được xác thực và biết rõ thì nhìn chung là ổn.
Trớ trêu là thứ thực sự hay gây vấn đề lại thường là API key. Tôi vừa phải triển khai cái này, và trong trường hợp của tôi, tôi cũng làm API key trông giống Bearer token, với tiền tố
sak.ngắn, tiếp theo là phần định danh (byte UUID base64url), rồi đến giá trị bí mật (byte base64url). Trong cơ sở dữ liệu, tôi lưu UUID và salt+hash ở mức passphrase được tạo từ giá trị bí mật. Vì vậy API key được tạo ra phải được xem là bí mật, và trong cơ sở dữ liệu nó chỉ được lưu theo một chiều, nên việc DB bị xâm nhập không đồng nghĩa với việc xác thực cũng bị xâm nhập.Dù vậy, rò rỉ API key vẫn có khả năng xảy ra cao hơn rất nhiều so với việc một giải pháp JWT được triển khai đúng gặp sự cố.
Tôi tình cờ thấy bài này, và vì trước đây đã làm khá nhiều về chủ đề này nên thấy thú vị khi nó lại được nhắc đến bây giờ. Nhưng bấm vào thì hóa ra tác giả có dẫn một phần tài liệu của tôi. Đúng là gợi lại ký ức từ rất lâu rồi.
Dù sao thì, đã có rất nhiều người thông minh hơn tôi bàn rất sâu về chủ đề này suốt nhiều năm, nhưng đến cả năm 2026 tôi vẫn nghĩ JWT là công cụ không phù hợp cho xác thực web. Dùng giữa các dịch vụ thì ổn, nhưng nếu có quyền chọn thì cứ dùng PASETO sẽ tốt hơn. Nó giải quyết được nhiều vấn đề.
https://www.paseto.io/
Hiện tôi đang gắn RabbitMQ vào website để dùng cho push notification. Tôi dùng xác thực JWT để kiểm soát việc client có thể đọc gì ở đâu, với thời hạn ngắn và làm mới token định kỳ.
Tôi không biết cấu hình nào khác mà lại gần được mức độ đơn giản của thiết lập này. Chỉ cần thêm một endpoint cấp token JWT cho phiên hợp lệ là xong, và còn có thể cấp quyền theo từng người dùng.
Một trong các bài được dẫn để giải thích vì sao không nên dùng JWT, nếu nhìn dễ tính thì cũng vẫn rất kỳ quặc.
https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-ba...
Tóm lại là “một số thư viện từng có lỗi”, rồi sau đó lại kéo libsodium vào và khuyên tự làm. Đây là lời khuyên vô lý đến mức khó mà xem là nghiêm túc. Mọi phần mềm đều có lỗi. Khi Heartbleed xảy ra thì cả Internet náo loạn, nhưng chúng ta vẫn tiếp tục dùng TLS và OpenSSL.
Tôi cũng mới lần đầu nghe câu “đặc tả JWT được thiết kế riêng cho token có thời hạn cực ngắn, khoảng dưới 5 phút”, và cũng không tìm thấy căn cứ nào để hậu thuẫn. RFC 7519 không có khẳng định như vậy.
Thông thường người ta dùng JWT như một bộ nhớ đệm xác thực. Nhận token xác thực từ dịch vụ xác thực, và token đó cấp quyền cho các dịch vụ khác.
Nó có nhiều lợi ích, nhưng cốt lõi là các dịch vụ cấp dưới không cần tương tác với cơ sở dữ liệu xác thực, cũng không cần có quyền phát hành token. Giả định là dùng RS256 chứ không phải HMAC. Vì vậy nếu dịch vụ cấp dưới bị xâm nhập thì vẫn không nghiêm trọng bằng việc một dịch vụ có thể truy cập cơ sở dữ liệu xác thực bị xâm nhập.
Nếu trong token có dữ liệu nhạy cảm thì phải dùng JWE, nhưng như vậy mỗi lần sử dụng lại phải yêu cầu một dịch vụ nội bộ giữ khóa riêng giải mã token, nên cũng không hẳn là hay lắm.
Cấu trúc tôi hay dùng là
{"id": (uuid), "scopes": ["scope:read/write"]}.Nó cũng khá phù hợp với SPA. Vì máy chủ site tĩnh có thể xác minh JWE bằng khóa công khai trước khi phục vụ tài nguyên. Cách tôi làm là biên dịch site tĩnh theo dạng
/(scope)/path, để dịch vụ tĩnh khỏi phục vụ ngay từ đầu những trang mà người truy cập không được phép xem. Điều này cực kỳ hữu ích khi bạn không muốn lộ cho người dùng các chức năng backend sở hữu, như bảng quản trị, hoặc các đường dẫn dịch vụ nội bộ có thể bị tấn công.JWT dùng cho “truy cập backend” có thời hạn khoảng 5 phút, còn những thứ như
/methì được cache trong localStorage trừ khi/refreshnói rõ là phải bỏ cache localStorage đi. Trình xử lý request của ứng dụng SPA sẽ phát hiện khi nào “cần làm mới” và tiến hành làm mới token.Tôi nghĩ phần lớn trách nhiệm ở đây nằm ở các thư viện node/next và Python. Backend được viết bằng ngôn ngữ có kiểu mạnh, còn frontend thì luôn là các trang tĩnh được biên dịch sẵn. Cấu hình frontend hiện tại dùng VITE, phần landing là các trang prerender, còn ứng dụng thì là SPA thông thường.
Dù đã cân nhắc tất cả những điều đó, tôi vẫn phản đối rất mạnh toàn bộ gist này. JWT có thể được làm cho an toàn đến mức bạn muốn.
JWT vẫn ổn, còn tiêu đề thì có vẻ hơi giật gân.
Thay vào đó có những chủ đề đáng để bàn hơn: khi nào nên dùng giá trị được mã hóa (đối xứng hoặc bất đối xứng), giá trị ngẫu nhiên nhưng bí mật, giá trị được ký (có thể đọc nhưng không thể sửa), nên đặt các giá trị đó ở đâu (bộ nhớ, localStorage, cookie), và làm sao để chúng không tồn tại mãi mãi cũng như liệu có cần hủy bỏ chúng trước thời điểm hết hạn tự nhiên hay không.