Hiểu về Device Bound Session Credentials (DBSC)

Bài viết này cung cấp phần giới thiệu về Device Bound Session Credentials (DBSC). Tôi sẽ mô tả vấn đề mà chúng cố gắng giải quyết, giao thức hoạt động như thế nào, và những gì bạn cần làm để hỗ trợ chúng.

Device Bound Session Credentials (DBSC) cố gắng giải quyết vấn đề gì?

Các ứng dụng web cần một cách để xác thực. Một trong những cách tiếp cận phổ biến (và hợp lý) nhất là dựa vào cookie tồn tại lâu dài (long-lived cookie) để xác thực người dùng. Với những cải tiến hiện đại như same-site cookie, nhiều lỗ hổng lịch sử liên quan đến xác thực bằng cookie giờ đây ít trở thành vấn đề hơn.

Tuy nhiên, một vấn đề cơ bản của cookie phiên xác thực vẫn còn: những cookie này là token bearer (mang theo quyền). Nghĩa là, không có cách nào để chứng minh bạn “sở hữu” cookie; nếu người khác có cookie, không có gì ngăn cản họ sử dụng nó.

Khi bạn nghe về token “bearer”, bạn thường nghĩ đến JWT token vốn thường được gửi trong header. Nhưng “bearer” đơn giản có nghĩa là “bất kỳ ai sở hữu token đều có thể sử dụng nó”, và điều này cũng áp dụng cho cookie.

Điều này dẫn đến rủi ro “ăn cắp cookie” và “hijacking phiên” (session hijacking): nếu kẻ tấn công tìm được cách đọc cookie xác thực của bạn, chúng có thể tự do sử dụng nó cho các hành vi độc hại, trên bất kỳ máy tính nào. Chúng không cần tìm cách gửi yêu cầu từ máy tính của nạn nhân; miễn là trích xuất được cookie, chúng có thể dùng nó từ máy của chính mình mà không gặp vấn đề gì.

Device Bound Session Credentials (DBSC) nhằm cung cấp cơ chế phòng vệ trước điểm yếu này, bằng cách đổi credential lâu dài lấy credential ngắn hạn, và sử dụng một refresh token yêu cầu máy chủ xác minh rằng nó đang được sử dụng trên đúng máy nó đã được cấp. Điều này làm cho việc hijacking phiên trở nên khó khăn hơn, vì bạn không còn có thể đơn giản trích xuất credential và dùng ở nơi khác; cookie ngắn hạn sẽ sớm hết hạn, và refresh token sẽ không còn hợp lệ.

Cơ chế xác minh này hoạt động bằng cách trình duyệt ký các yêu cầu bằng một private key được lưu trong Trusted Platform Module (TPM). Key không bao giờ bị lộ ra ngoài TPM, nên bạn có thể chắc chắn rằng nếu một yêu cầu được ký bằng cùng một key, nó đến từ cùng một máy.

DBSC cũng nhằm mục đích cung cấp progressive enhancement (nâng cấp dần), và không yêu cầu thay đổi cách các ứng dụng web làm xác thực. Máy chủ có thể yêu cầu trình duyệt sử dụng DBSC khi có thể, và nếu trình duyệt hỗ trợ, nó sẽ tự động tham gia một cách liền mạch. Trình duyệt không hỗ trợ DBSC sẽ không nhận ra header opt-in, và do đó tiếp tục hoạt động như trước; không có breaking change nào.

DBSC hoạt động như thế nào?

Trong phần này, tôi mô tả luồng tổng thể cho một máy chủ và trình duyệt triển khai DBSC. Đây là cái nhìn tổng quan rất chung, và chắc chắn sẽ có nhiều sắc thái trong bất kỳ triển khai thực tế nào (ví dụ xem bài viết của Scott Helme về các edge case và lỗi ông gặp phải). Mục tiêu là theo dõi các yêu cầu bổ sung diễn ra, để bạn hiểu rõ hơn những gì đang xảy ra nếu bạn chọn triển khai DBSC trong các ứng dụng của mình.

Triển khai DBSC

Triển khai DBSC trên máy chủ nên tương đối đơn giản (so với một số biện pháp bảo mật khác). Tóm lại, ứng dụng cần:

  • Thêm header Secure-Session-Registration vào response khi người dùng đăng nhập. Header này báo cho trình duyệt biết máy chủ hỗ trợ DBSC.
  • Triển khai một endpoint “session registration” (đăng ký phiên) để đăng ký phiên trình duyệt và chuyển sang cookie ngắn hạn.
  • Triển khai một endpoint “refresh” để xác thực rằng trình duyệt vẫn còn quyền truy cập key, và làm mới cookie phiên ngắn hạn.

Một khía cạnh quan trọng của DBSC là bạn không cần thay đổi cách bạn làm xác thực và validation về tổng thể. Bạn cần thêm các endpoint nói trên, và xử lý các hệ quả của chúng, nhưng nói chung mã xác thực của bạn sẽ chỉ tiếp tục hoạt động như hiện tại.

Ngoài ra, luồng này là tùy chọn. Nếu trình duyệt không nhận ra header Secure-Session-Registration, nghĩa là nó không hỗ trợ DBSC, và không có gì xảy ra. Ứng dụng của bạn tiếp tục hoạt động y như trước khi bạn triển khai DBSC.

Sau khi biết bạn chỉ cần triển khai vài endpoint, hãy cùng xem luồng end-to-end của DBSC.

Luồng DBSC

Luồng tổng thể của các yêu cầu, sau khi người dùng đăng nhập vào một máy chủ hỗ trợ DBSC, được thể hiện trong sơ đồ sau:

Luồng DBSC

Lưu ý rằng có sẵn các pattern tích hợp thay thế, vì vậy bạn có thể thấy các biến thể khác của sơ đồ trên.

1. Response đăng nhập bao gồm header Secure-Session-Registration

Bước đầu tiên là máy chủ báo cho trình duyệt biết rằng nó hỗ trợ DBSC. Khi người dùng đăng nhập, ngoài cookie xác thực thông thường, máy chủ thêm một header bổ sung vào response, Secure-Session-Registration:

HTTP/1.1 200 OK
Secure-Session-Registration: (ES256 RS256); path="/dbsc/registration";challenge="4a96e2cab06e76e2366b5e802bfcbabfe81e52c81abfcc71afc97010157ae9bd"
Set-Cookie: MyAuthCookie=CfDJ8Apn9==; path=/; samesite=lax; httponly

Cấu trúc của Secure-Session-Registration như sau:

  • (ES256 RS256): Đây là các thuật toán được hỗ trợ mà trình duyệt có thể dùng để ký. Trong trường hợp này, các thuật toán được hỗ trợ là ECDSA P-256 và RSASSA-PKCS1-v1_5.
  • path="/.well-known/dbsc/registration": Đây là đường dẫn “registration” mà trình duyệt nên dùng để đăng ký các DBSC credential mới.
  • challenge="<somevalue>": Một giá trị ngẫu nhiên mà trình duyệt sẽ ký và gửi như một phần của lời gọi registration.

Có một số trường bổ sung khác có thể được đưa vào header Secure-Session-Registration, nhưng không phải mọi triển khai nào cũng yêu cầu, chẳng hạn như authorization hay provider_key, nhưng tôi sẽ bỏ qua chúng ở đây. Bạn có thể đọc thêm về chúng trong specification tại đây.

Khi trình duyệt nhận response và thấy header Secure-Session-Registration, đây chính là tín hiệu để nó đăng ký một key với DBSC endpoint mà ứng dụng của bạn mở ra.

2. Trình duyệt gửi yêu cầu đến endpoint registration

Đây là bước then chốt trong luồng. Khi nhận được header Secure-Session-Registration, trình duyệt thực hiện các việc sau:

  • Tạo một cặp public-private key mới trong TPM/secure enclave.
  • Ký challenge được cung cấp bằng private key.
  • Gửi yêu cầu POST đến path được chỉ định, chứa challenge đã ký và public key, dưới dạng một JWT.

Vì vậy trình duyệt gửi một thứ gì đó như thế này:

POST /dbsc/registration HTTP/1.1
Cookie: MyAuthCookie=CfDJ8Apn9==
Secure-Session-Response: eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImxITjNhci13bFZTU0FkeThPSlhxeGhId0JpdXVyMnJUbG1ieGNnaW05X28iLCJ5IjoiemZfeTc5cDhycGI3enRuUUdaMjV4UFhfTHFfcjdvMUNoOWp2bmU3MHRKNCJ9LCJ0eXAiOiJkYnNjK2p3dCJ9.eyJqdGkiOiI0YTk2ZTJjYWIwNmU3NmUyMzY2YjVlODAyYmZjYmFiZmU4MWU1MmM4MWFiZmNjNzFhZmM5NzAxMDE1N2FlOWJkIn0.MO28tc5E-OwA7IlKTAMe1yCGkRA_b8ljLVpP0gnc-jky7g1dcw2CrYREB0KWoE_ae5ixCjJZc4IEcVwfnP_qKA
Content-Length: 0

Như bạn có thể thấy, đây là POST đến Path được cung cấp trong header ban đầu, và nó bao gồm cookie xác thực gốc. Secure-Session-Response chứa một JWT được mã hóa base64. Nếu bạn đưa nó vào một decoder, bạn sẽ thấy nó chứa:

  • Header, chỉ định thuật toán dùng để ký, cùng với public key.
  • Payload, chỉ chứa jti, tiếp theo là challenge được gửi trong header.
  • Signature, được tạo từ header và payload, cùng với private key.

Header đã giải mã ở trên trông như thế này:

{
  "alg": "ES256",
  "jwk": {
    "crv": "P-256",
    "kty": "EC",
    "x": "lHN3ar-wlVSSAdy8OJXqxhHwBiuur2rTlmbxcgim9_o",
    "y": "zf_y79p8rpb7ztnQGZ25xPX_Lq_r7o1Ch9jvne70tJ4"
  },
  "typ": "dbsc+jwt"
}

trong khi body trông như thế này:

{
  "jti": "4a96e2cab06e76e2366b5e802bfcbabfe81e52c81abfcc71afc97010157ae9bd"
}

Sau đó, việc xử lý yêu cầu này thuộc về máy chủ.

3. Máy chủ xác thực yêu cầu và trả về credential ngắn hạn

Khi máy chủ nhận yêu cầu này, nó phải trước tiên xác thực JWT trong header Secure-Session-Response, và xác nhận rằng:

  1. Chữ ký JWT là chính xác và hợp lệ.
  2. Có một cookie xác thực hợp lệ cho người dùng.
  3. Challenge trong JWT khớp với challenge được đưa vào header Secure-Session-Registration ban đầu.

Nếu tất cả đều đúng, máy chủ nên thực hiện các việc sau:

  1. Tạo một DBSC session ID mới, và liên kết nó với người dùng hiện tại.
  2. Thay cookie xác thực hiện có bằng một cookie ngắn hạn.
  3. Trả về các hướng dẫn trong response về cách làm mới cookie ngắn hạn, và cookie nên được sử dụng ở đâu.

Nếu mọi thứ suôn sẻ, response sẽ trông giống như thế này:

HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: MyDbscCookie=abc123; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=300

{
  "session_identifier": "199d6f60681",
  "refresh_url": "/dbsc/refresh",
  "scope": {
    "origin": "https://example.com",
    "include_site": false
  },
  "credentials": [
    {
      "type": "cookie",
      "name": "MyDbscCookie",
      "attributes": "Path=/; Secure; HttpOnly; SameSite=Lax"
    }
  ]
}

Hãy cùng phân tích từng phần của response này:

  • Cookie MyAuthCookie lâu dài không còn xuất hiện nữa.
  • Một cookie MyDbscCookie mới giờ được đặt, với thời gian hết hạn ngắn (5 phút). Cookie này giờ đóng vai trò là cookie xác thực cho người dùng.
  • Body chứa session_identifier, dùng để liên kết DBSC session này với người dùng hiện tại. Trình duyệt sẽ bao gồm giá trị này bất cứ khi nào nó cần làm mới cookie xác thực ngắn hạn.
  • Body chứa refresh_url, tức là đường dẫn mà trình duyệt phải gọi đến để lấy một cookie xác thực ngắn hạn mới.
  • scope nói cookie ngắn hạn hợp lệ ở đâu, và không nên được sử dụng ở đâu.
  • Cuối cùng, phần credits cung cấp chi tiết về chính xác cookie mà cấu hình này áp dụng cho.

Lưu ý rằng giá trị hợp lệ duy nhất cho "type" là cookie, vì vậy nhiều khả năng đây chỉ là cơ chế phòng ngừa cho tương lai.

Sau khi nhận response, trình duyệt sẽ sử dụng các credential này cho tất cả các yêu cầu tiếp theo. Máy chủ phải sử dụng cookie ngắn hạn thay cho cookie xác thực “thông thường”, và mọi thứ hoạt động như “bình thường” ngoại trừ điều đó.

Tất nhiên, chẳng bao lâu nữa cookie đó sẽ hết hạn, vì vậy điều quan trọng là trình duyệt phải có thể làm mới các credential này.

4. Trình duyệt làm mới credential trong nền

Khi trình duyệt cần gửi yêu cầu đến ứng dụng của bạn, và cookie ngắn hạn đã hết hạn, trình duyệt cần lấy một phiên bản mới của cookie. Nói chung, trình duyệt có thể sẽ cố gắng làm mới trước khi cookie hết hạn, nhưng nếu không, chúng sẽ hoãn yêu cầu của người dùng cho đến sau khi yêu cầu refresh hoàn tất.

Trình duyệt trước tiên gửi một yêu cầu đến endpoint refresh, bao gồm DBSC session ID:

POST /dbsc/refresh HTTP/1.1
Sec-Secure-Session-Id: 199d6f60681

Máy chủ sau đó tạo một challenge mới để trình duyệt ký trong header Secure-Session-Challenge và trả về response 403:

HTTP/1.1 403 Forbidden
Secure-Session-Challenge: "4524d32ab2b9";id="199d6f60681"

Tiếp theo, trình duyệt tạo một JWT được ký khác, chứa giá trị challenge bằng cùng private key mà nó đã dùng để đăng ký phiên ban đầu. Sau đó nó gửi một yêu cầu khác đến cùng endpoint refresh, nhưng lần này với JWT được đưa vào header Secure-Session-Response:

POST /dbsc/refresh HTTP/1.1
Sec-Secure-Session-Id: 199d6f60681
Secure-Session-Response: eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJ...

Máy chủ sau đó phải đảm bảo mọi thứ khớp với nhau:

  1. Chữ ký JWT là chính xác và hợp lệ.
  2. Session ID là một phiên hiện tại đã biết.
  3. Challenge chứa trong JWT khớp với challenge đã gửi cho trình duyệt.

Nếu tất cả các kiểm tra đó đạt, máy chủ tạo một cookie ngắn hạn mới, và phản hồi với 200 OK:

HTTP/1.1 200 OK
Set-Cookie: MyDbscCookie=def456; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=300

Khi trình duyệt nhận response, nó thay cookie ngắn hạn đã hết hạn bằng cookie mới, và tiếp tục với yêu cầu bị hoãn.

Và đó là toàn bộ DBSC. Khi cookie hết hạn, trình duyệt tiếp tục gọi endpoint refresh để tạo các cookie ngắn hạn mới.

Bạn có thể dùng DBSC không? Nó có đáng không?

Vậy, với tư cách là người tạo ứng dụng, bạn có nên hỗ trợ DBSC không? Nói chung, tôi nghĩ câu trả lời là “có lẽ”, vì nó thực sự không có bất kỳ nhược điểm rõ ràng nào, và nó bảo vệ người dùng của bạn khỏi session hijacking.

Hiện tại, chỉ có Chromium triển khai DBSC, nên mặc dù đó là thị phần lớn, nhưng vẫn chưa phải là phổ biến khắp nơi. Tin tốt là bạn nên có thể triển khai DBSC, và sau đó nó chỉ kích hoạt nếu trình duyệt của người dùng hỗ trợ. Nếu trình duyệt không hỗ trợ DBSC, nó sẽ hoàn toàn bỏ qua header Secure-Session-Registration, và trình duyệt đơn giản sử dụng cookie lâu dài/phiên thông thường như bình thường.

Nhưng nếu trình duyệt có hỗ trợ DBSC, bạn sẽ nhận được tất cả những lợi ích mà nó mang lại. Cụ thể, sự bảo vệ chống lại session-hijacking, bằng cách chuyển sang credential ngắn hạn. Và nó không yêu cầu những thay đổi lớn trong ứng dụng của bạn. Vậy tại sao không?

Có một số khó khăn tiềm ẩn với việc triển khai, như được mô tả trong bài viết này của Scott Helme, vốn có thể rất gây rắc rối trong một số trường hợp. Vì lý do đó, tôi khuyên bạn nên chờ framework triển khai tính năng này. Ở mức độ nhỏ hơn, tôi nhận thấy trình chặn quảng cáo của mình hoàn toàn chặn luồng DBSC 😅

Vậy kết luận: có, hãy triển khai nó để có thêm bảo mật, nhưng có thể hãy chờ một triển khai chính thức (canonical) trong ngôn ngữ/framework bạn chọn trước đã!

Tài liệu tham khảo

Tôi viết bài dựa trên việc đọc nhiều bài khác, và tôi khuyên bạn nên xem các nguồn sau để hiểu rõ hơn về tính năng này: