Nơi lưu trữ JWT - Cookies với HTML5 Web Storage

Bài viết này được dịch và chỉnh sửa một chút từ bài gốc: Where to Store your JWTs – Cookies vs HTML5 Web Storage của tác giả Tom Abbott.


Gần đây Stormpath phát triển tính năng xác thực bằng token sử dụng JSON Web Tokens (JWT) và chúng tôi đã có nhiều cuộc thảo luận về tính bảo mật của những token này và nơi lưu trữ chúng.

Nếu bạn tò mò xem bạn nên lựa chọn phương pháp nào thì bài viết này là dành cho bạn. Chúng ta sẽ nói về những thứ cơ bản của JSON Web Tokens (JWT), lưu trữ token ở cookies với HTML5 web storage (localStorage hoặc sessionStorage) và các thông tin cơ bản về cross-site scripting (XSS) và cross-site request forgery (CSRF).

Hãy cùng bắt đầu...

JSON Web Tokens (JWT): Khái niệm cơ bản

Các giải pháp xác thực và định quyền bằng API được triển khai nhiều nhất là OAuth 2.0 và đặc tả JWT. Dưới đây là những thứ bạn cần biết về JWT:

  • JWT là một cơ chế xác thực tuyệt vời. Nó cho phép bạn định nghĩa người dùng và quyền của họ theo cấu trúc và không trạng thái. Nó có thể được ký bằng mật mã và mã hóa để tránh bị giả mạo từ phía client.

  • JWT là một cách khai báo thông tin về token và xác thực tuyệt vời. Bạn có thể thoải mái quyết định xem thứ gì có ý nghĩa đối với ứng dụng của mình vì bạn đang làm việc với JSON.

  • Khái niệm về phạm vi truy cập rất mạnh mẽ và vô cùng đơn giản: bạn tự do thiết kế ngôn ngữ kiểm soát truy cập của riêng mình vì bạn đang làm việc với JSON.

Nếu bạn gặp một token trong thực tế, nó sẽ trông như sau:

"dBjftJeZ4CVP.mB92K27uhbUJU1p1r.wW1gFWFOEjXk…"

Đó là một xâu được mã hóa Base64. Nếu bạn tách nó ra, bạn sẽ được 3 phần riêng biệt

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
.
eyJpc3MiOiJodHRwOi8vZ2FsYXhpZXMuY29tIiwiZXhwIjoxMzAwODE5MzgwLCJzY29wZXMiOlsiZXhwbG9yZXIiLCJzb2xhci1oYXJ2ZXN0ZXIiXSwic3ViIjoic3RhbmxleUBhbmRyb21lZGEuY29tIn0
.
edK9cpfKKlGtT6BqaVy4bHnk5QUsbnbYCWjBEE7wcuY

Phần đầu là header mô tả về token. Phần thứ 2 là một payload chứa các phần thông tin quan trọng nhất, và phần thứ 3 là chữ ký được sử dụng để xác minh tính nguyên vẹn của token (nếu bạn có khóa bí mật thì nó sẽ được sử dụng để tạo chữ ký).

Khi chúng ta giải mã phần thứ 2, payload, chúng ta sẽ thu được một object JSON:

{
  "iss": "http://galaxies.com",
  "exp": 1300819380,
  "scopes": ["explorer", "solar-harvester", "seller"],
  "sub": "[email protected]"
}

Trên đây là payload của token. Nó cho bạn biết những thứ sau:

  • Người này là ai (sub, viết tắt của subject - chủ thể)
  • Người này có thể truy cập những gì với token này (phạm vi truy cập)
  • Khi nào token này hết hạn (exp)
  • Ai cấp phát token này (iss, viết tắt của issuer - người cấp phát)

Những khai báo này được gọi là các "tuyên bố" vì người tạo ra token này tuyên bố một tập các xác nhận được sử dụng để "biết" các thông tin về chủ thể. Vì token được ký với một khóa bí mật, bạn có thể xác định chữ ký là thật hay giả và ngầm tin tưởng những gì token tuyên bố.

Tokens được gửi cho người dùng sau khi họ cung cấp một số thông tin xác thực, thông thường là tên người dùng và mật khẩu, nhưng họ cũng có thể cung cấp API key, hay thậm chí là token từ dịch vụ khác. Điều này quan trọng vì việc gửi một token (có thể hết hạn, và có phạm vi truy cập bị giới hạn) tới API của bạn sẽ tốt hơn việc gửi tên người dùng và mật khẩu. Nếu tên người dùng và mật khẩu bị lấy cắp bởi tấn công man-in-the-middle thì kẻ tấn công đã có chìa khóa để vào được lâu đài.

Tính năng xác thực bằng API key của Stormpath là một ví dụ. Ý tưởng của tính năng này là bạn cung cấp thông tin xác thực cố định của mình một lần, sau đó lấy được một token để sử dụng thay cho các thông tin xác thực cố định đó.

Đặc tả JSON Web Token (JWT) nhanh chóng thu hút được sự chú ý. Rất được khuyến khích bởi Stormpath, nó cung cấp cấu trúc và tính bảo mật cùng với sự linh hoạt để có thể được tùy biến cho phù hợp với ứng dụng của bạn. Và đây là một bài viết dài về nó: Sử dụng JWT đúng cách!.

Nơi lưu trữ JWT của bạn

Giờ chúng ta đã hiểu JWT là gì, bước tiếp theo là tìm cách lưu trữ các token này. Nếu bạn đang xây dựng một ứng dụng web, bạn có một số lựa chọn:

  • HTML5 Web Storage (localStorage hoặc sessionStorage)
  • Cookies

Để so sánh 2 cách lưu trữ này, hãy giả sử chúng ta có một ứng dụng AngularJS hoặc ứng dụng đơn trang (SPA) được gọi là galaxies.com với một login route (/token) để xác thực người dùng và trả về một JWT. Để truy cập các API endpoint của SPA, client cần phải cung cấp một JWT hợp lệ.

Yêu cầu mà SPA gửi lên server có dạng như sau:

HTTP/1.1

POST /token
Host: galaxies.com
Content-Type: application/x-www-form-urlencoded

[email protected]&password=andromedaisheadingstraightforusomg&grant_type=password

Phản hồi của server sẽ thay đổi tùy vào việc bạn sử dụng cookie hay Web Storage. Để so sánh, hãy xem xét cả 2 cách.

JWT được lưu ở localStorage hoặc sessionStorage (Web Storage)

Đổi từ cặp tên người dùng và mật khẩu lấy một JWT và lưu nó ở browser storage (sessionStorage hay localStorage) khá là đơn giản. Phần thân của phản hồi sẽ chứa JWT:

HTTP/1.1 200 OK

  {
  "access_token": "eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB",
    "expires_in":3600
  }

Ở phía client, bạn sẽ lưu token ở HTML5 Web Storage (giả sử chúng ta có một success callback):

function tokenSuccess(err, response) {
    if(err){
        throw err;
    }
    $window.sessionStorage.accessToken = response.body.access_token;
}

Để gửi access token lên API, bạn sẽ phải sử dụng HTTP Authorization Header và Bearer scheme. Yêu cầu mà SPA gửi lên server sẽ có dạng như sau:

HTTP/1.1

GET /stars/pollux
Host: galaxies.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB

JWT được lưu ở cookie

Việc đổi cặp tên người dùng và mật khẩu lấy JWT và lưu nó ở cookie cũng đơn giản như vậy. Phản hồi từ server sẽ sử dụng HTTP header Set-Cookie:

HTTP/1.1 200 OK

Set-Cookie: access_token=eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB; Secure; HttpOnly;

Để truyền access token lên API trên cùng domain, trình duyệt sẽ tự động gửi kèm cookie. Yêu cầu gửi lên API sẽ có dạng:

GET /stars/pollux
Host: galaxies.com

Cookie: access_token=eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB;

Vậy điểm khác biệt là gì?

Nếu bạn so sánh hai phương pháp, cả hai đều nhận JWT ở browser. Cả hai đều không lưu trạng thái vì tất cả các thông tin mà API cần là JWT. Cả hai đều gửi token lên API một cách đơn giản. Điểm khác biệt nằm ở tính bảo mật của chúng.

Tính bảo mật của JWT lưu trữ trên sessionStorage và localStorage

Web Storage (localStorage/sessionStorage) có thể truy cập được thông qua JavaScript trên cùng domain. Điều này nghĩa là bất cứ code JavaScript nào chạy trên trang của bạn cũng có thể truy cập vào web storage, và khiến cho trang của bạn có thể bị tấn công bằng XSS. XSS, về cơ bản, là một dạng lỗ hổng mà kẻ tấn công có thể chèn JavaScript có thể chạy được vào trang của bạn. Kiểu tấn công XSS cơ bản là cố gắng chèn JavaScript thông qua form input, kẻ tấn công sẽ đặt <script>alert('You are Hacked')</script> vào form và xem xem liệu nó có chạy trên browser và có thể thấy được bởi những người dùng khác hay không.

Để chống XSS, phương pháp thông thường là escape và encode tất cả các dữ liệu không tin tưởng được. Nhưng chưa phải là đã hết. Thời điểm năm 2015, các ứng dụng web hiện đại sử dụng JavaScript lưu trữ trên CDS hoặc hạ tầng mạng ngoài. Các ứng dụng web hiện đại sử dụng các thư viện JavaScript bên thứ 3 để kiểm tra A/B, phân tích funnel/market và quảng cáo. Chúng ta sử dụng các package manager như Bower để sử dụng code của người khác trong ứng dụng của mình.

Điều gì xảy ra nếu một trong số các đoạn script chúng ta sử dụng đã bị nhiễm độc? JavaScript độc hại có thể được nhúng trong trang, và Web Storage sẽ bị rò rỉ. Những kiểu tấn công XSS này có thể lấy Web Storage của tất cả mọi người truy cập vào trang của bạn mà họ không hề hay biết. Đây có thể là lý do mà một nhóm các tổ chức khuyên không nên lưu trữ bất cứ thứ gì giá trị vào hay tin bất cứ thông tin nào từ web storage. Những thứ này bao gồm session identifier và token.

Là một cơ chế lưu trữ, Web Storage không bắt buộc áp dụng bất cứ một tiêu chuẩn bảo mật nào trong quá trình gửi nhận. Bất cứ ai đọc Web Storage và sử dụng nó phải đảm bảo rằng họ luôn gửi JWT qua HTTPS chứ không phải HTTP.

Tính bảo mật của JWT lưu trữ trên cookie

Cookie, khi được dùng với cookie flag HttpOnly, không thể bị truy cập bởi JavaScript, và miễn nhiễm với XSS. Bạn cũng có thể đặt cookie flag Secure để đảm bảo rằng cookie chỉ được gửi qua HTTPS. Đây là một trong những lý do chính mà trong quá khứ cookie được tận dụng để lưu token hay dữ liệu session. Các lập trình viên hiện đại do dự khi sử dụng cookie vì cookie bắt buộc lưu trạng thái trên server, do đó phá vỡ best practive của RESTful. Cookie được sử dụng để lưu JWT sẽ không bắt buộc lưu trạng thái trên server. Điều này bởi vì JWT đóng gói mọi thứ server cần để xử lý yêu cầu của client.

Tuy nhiên, cookie có thể bị tấn công bởi một dạng tấn công khác: cross-site request forgery (CSRF). Tấn công CSRF là một kiểu tấn công xảy ra khi các trang web, email hay blog độc hại khiến trình duyệt của người dùng thực thi các hành động không mong muốn trên một trang tin tưởng được mà hiện tại người dùng đã được xác thực trên trang đó. Đây là một lỗ hổng ở cách trình duyệt xử lý cookies. Một cookie chỉ có thể được gửi tới các domain mà nó được phép. Mặc định, domain đó là domain đã đặt cookie lên trình duyệt. Cookie sẽ được gửi đi cho dù bạn đang ở trang galaxies.com hay hahagonnahackyou.com.

CSRF hoặc động bằng cách cố gắng dụ bạn vào trang hahagonnahackyou.com. Trang đó sẽ có một thẻ img hoặc code JavaScript để giả lập một form post lên galaxies.com và cố gằng cướp lấy session của bạn, nếu nó vẫn còn hợp lệ, và thay đổi tài khoản của bạn.

Ví dụ:

<body>

  <!-- CSRF with an img tag -->

  <img href="http://galaxies.com/stars/[email protected]" />

  <!-- or with a hidden form post -->

  <script type="text/javascript">
  $(document).ready(function() {
    window.document.forms[0].submit();
  });
  </script>

  <div style="display:none;">
    <form action="http://galaxies.com/stars/pollux" method="POST">
      <input name="transferTo" value="[email protected]" />
    <form>
  </div>
</body>

Cả hai sẽ gửi cookie tới galaxies.com và có thể gây ra những thay đổi trạng thái không mong muốn. CSRF có thể bị ngăn chặn bằng cách sử dụng mẫu token đồng bộ. Việc này nghe có vẻ phức tạp, nhưng tất cả các web framework hiện đại đều hỗ trợ nó.

Ví dụ, AngularJS có một giải pháp để xác nhận rằng cookie chỉ có thể được truy cập bởi domain của bạn. Từ tài liệu của AngularJS:

Khi thực thi XHR request, service $http sẽ đọc token từ cookie (mặc định là XSRF-TOKEN) và đặt nó trong một HTTP header (X-XSRF-TOKEN). Do chỉ có JavaScript chạy trên domain của bạn có thể đọc cookie, server của bạn sẽ đảm bảo rằng XHR đến từ JavaScript chạy trên domain của bạn.

Bạn có thể làm cho việc chống CSRF này trở nên không trạng thái bằng cách thêm một tuyên bố JWT là xsrfToken:

{
  "iss": "http://galaxies.com",
  "exp": 1300819380,
  "scopes": ["explorer", "solar-harvester", "seller"],
  "sub": "[email protected]",
  "xsrfToken": "d9b9714c-7ac0-42e0-8696-2dae95dbc33e"
}

Nếu bạn sử dụng Stormpath SDK dành cho AngularJS, bạn sẽ phòng chống được CSRF một cách không trạng thái mà không tốn công tự phát triển.

Tận dụng sự bảo vệ khỏi CSRF của web framework mà bạn sử dụng để phát triển ứng dụng khiến cho việc sử dụng cookie để lưu trữ JWT trở nên đảm bảo. CSRF cũng có thể bị ngăn chặn một phần bằng cách kiểm tra HTTP header RefererOrigin từ API của bạn. Tấn công CSRF sẽ có RefererOrigin không liên quan tới ứng dụng của bạn.

Cho dù việc lưu JWT có bảo mật hơn, cookie có thể khiến cho một số lập trình viên đau đầu, nếu ứng dụng của bạn yêu cầu truy cập cross-domain để hoạt động. Bạn nên biết rằng cookie có các thuộc tính phụ trợ (Domain/Path) có thể bị thay đổi để cho phép bạn chỉ định nơi mà cookie được phép gửi đi. Sử dụng AJAX, server có thể báo cho trình duyệt biết liệu thông tin xác thực (bao gồm cookie) có thể được gửi với request sử dụng CORS hay không.

Kết luận

JWT là một cơ chế xác thực tuyệt vời. Nó cho phép bạn khai báo thông tin người dùng và phạm vi truy cập của họ một cách có cấu trúc. Nó có thể được mã hóa và ký để chống giả mạo từ phía client, nhưng điểm bất lợi nằm ở cách bạn lưu trữ chúng. Stormpath khuyên bạn nên lưu JWT ở cookie, vì sự bảo mật mà cách này cung cấp và sự bảo vệ trước CSRF một cách đơn giản ở các web framework hiện đại. HTML5 Web Storage có thể bị tấn công XSS, có vùng tấn công lớn và có thể ảnh hướng tới tất cả người dùng của ứng dụng nếu tấn công thành công.