Có một câu hỏi tuy đơn giản nhưng vẫn nhiều bạn còn bỡ ngỡ là "Làm sao để lưu token ở browser một cách chuẩn chỉ, an toàn và hiệu quả nhất" thì trong bài viết này sẽ giải đáp thắc mắc đó 🤭
Token là gì ?
JWT (JSON Web Token) là một đoạn mã được mã hóa chứa thông tin xác thực và ủy quyền trong ứng dụng web và di động. Nó bao gồm ba phần chính: Header, Payload và Signature. JWT có tính di động, không cần lưu trạng thái phiên làm việc trên máy chủ và giúp cải thiện hiệu suất. Tuy nhiên, việc sử dụng JWT cũng cần cân nhắc về việc bảo mật. Khi JWT được ký bằng một secret key, cần đảm bảo rằng secret key được bảo vệ cẩn thận để tránh việc token bị giả mạo hoặc biên tập phục vụ cho các mục đích xấu.
Vấn đề đặt ra
Từ kinh nghiệm cá nhân, mình thấy rất nhiều ứng dụng sử dụng cách lưu trữ ở local storage hay session storage cũng như dùng unsecured cookie. Những cách này bộc lộ rất nhiều điểm yếu như rất dễ dàng bị tấn công bởi XSS, CSRF dẫn đến thông tin cá nhân bị lộ ra ngoài.
Vì lí do đó, bài viết này chúng ta sẽ cùng nhau ngó qua các cách để thay thế như Closure, Secure Cookie và Service Worker sử dụng Jquery phía client và Nodejs phía backend để thực thi tạo và xác thực API.
Source code mình để ở đây https://github.com/quocthinh861/jwt-token-storage
Khám phá
Ứng dụng được chia thành 2 phần: frontend và backend
Mỗi thư mục sẽ có riêng file package.json và được chạy độc lập với câu lệnh npm start
Phần frontend được chạy ở port 4000, phần backend thì port 3000. Khi khởi chạy thành công, vào http://localhost:4000
sẽ hiện thị như hình bên dưới
Khi người dùng chưa đăng nhập thì nút login, logout sẽ bị vô hiệu hóa.
Closure
Kỹ thuật này nhằm giữ giá trị "trong bộ nhớ" của token bên trong một hàm ẩn danh (hay closure). Như vậy, biến có phạm vi cục bộ và không thể đọc hoặc truy cập được bởi bất kỳ script hoặc phần mềm khác.
Trong code ở nhánh master, token được bao bọc trong hàm “$(document).ready”
Khi người dùng ấn đăng nhập, API "/login" sẽ được gọi và JWT Token sẽ được gửi về phía client
JWT token hết hạn sau 5 phút, vì vậy sau khi đăng nhập, refreshToken được gọi mỗi 4 phút để duy trì tính hợp lệ của token.
Kỹ thuật này an toàn và rất đơn giản để triển khai, tuy nhiên, mỗi khi người dùng làm mới trang, token sẽ bị mất và bạn cần phải đăng nhập lại.
Secure Cookie
Một kỹ thuật khác, được sử dụng phổ biến, bao gồm việc gửi token thông qua một cookie với các tùy chọn sau:
- sameSite: true → để áp dụng quy định same site chặt chẽ
- httpOnly: true → không cho phép JavaScript đọc cookie
- secure: true → để gửi cookie trở lại máy chủ trong tương lai nếu trình duyệt chỉ có kết nối HTTPS
- maxAge: number → không nên vượt quá 30 phút
Nhược điểm duy nhất của kỹ thuật này là cookie sẽ không khả dụng qua các miền khác nhau (cross-domain).
Service Worker
Và cuối cùng một kỹ thuật an toàn nhất nhưng hơi phức tạp. Service Worker chạy trên một luồng riêng biệt, cho phép token vẫn khả dụng ngay cả sau khi tải lại trang.
Service Worker hoạt động như một script nền chạy độc lập với luồng chính của trình duyệt. Nó có thể chặn các yêu cầu mạng, xử lý việc lưu cache và thực hiện nhiều nhiệm vụ khác, bao gồm quản lý các token xác thực.
Bằng cách lưu trữ token trong phạm vi của Service Worker, token vẫn khả dụng ngay cả khi trang web được tải lại hoặc đóng và mở lại. Điều này đảm bảo người dùng vẫn được xác thực và có thể tiếp tục truy cập các tài nguyên được bảo vệ một cách mượt mà.
Triển khai kỹ thuật này yêu cầu thiết lập và cấu hình Service Worker trong ứng dụng web của bạn. Service Worker sẽ chặn các yêu cầu và kiểm tra sự tồn tại của một token hợp lệ, sau đó cung cấp token đó cho ứng dụng ngay cả khi trang web được tải lại.
Page reload
Khi tải lại trang, chúng ta kích hoạt service worker và kiểm tra sự tồn tại của một token. Nếu có, chúng ta xác thực token bằng cách gửi một yêu cầu tới máy chủ.
Khi service worker đã sẵn sàng, chúng ta gọi refresh token để kiểm tra xem token còn hợp lệ hay không.
Set the token
Token chỉ nên được thiết lập thông qua service worker trong phản hồi đăng nhập để tránh bị các cuộc tấn công XSS hoặc các đoạn mã độc hại chặn lấy thông tin.
Khi đăng xuất hoặc khi xảy ra lỗi, người dùng thông báo cho service worker để xóa token.
Hãy chắc chắn rằng kết quả trả về cho người dùng sẽ không bao gồm token.
fetch method interception
Nhìn qua service worker, chúng ta lắng nghe phương thức fetch để tiêm vào token khi nó có sẵn:
Lưu ý: XMLHttpRequest ($.ajax) không thể bị can thiệp bởi service worker, do đó chúng ta sử dụng fetch thay thế.
Tuy nhiên, khi sử dụng fetch, chúng ta cần chú ý bảo vệ nó khỏi XSS payload:
Eververgreen browsers
Mặc dù kỹ thuật này an toàn và hiệu quả trong việc xử lý việc tải lại trang, nhưng cần lưu ý rằng không tất cả các trình duyệt đều hỗ trợ service worker. Service worker là một tính năng của các trình duyệt hiện đại, được gọi là evergreen browsers, tuân thủ các tiêu chuẩn web mới nhất.
Kết bài
Trong bài viết này, chúng tôi đã cố gắng khám phá ba kỹ thuật để lưu trữ token một cách đúng đắn trên các trình duyệt.
Việc triển khai mà tôi cung cấp không sử dụng HTTPS. Mục đích chỉ là tương tác với một backend chéo miền và minh họa kỹ thuật.
Mã nguồn đầy đủ có sẵn tại: https://github.com/quocthinh861/jwt-token-storage
Mỗi kỹ thuật được triển khai tương ứng trong một nhánh cụ thể: closure, cookie và service-worker.