So sánh Authentication và Cookies

Cookie, session, token, JWT, lưu token ở đâu, các mối quan tâm về xác thực trong một hệ thống Single-Page Application... tất cả mọi thứ bạn cần biết đều ở đây.

TL;DR;

Có thể triển khai Authentication [xác thực] trong single page application [SPA] với nhiều mô hình có ưu điểm, nhược điểm riêng. Bài này sẽ nói về các concept [khái niệm] quan trọng bạn cần biết khi xử lí với user authentication, đặc biệt là trong kiến trúc xây dựng SPA khá phổ biến hiện nay:

Điều kiện tiên quyết về bảo mật

Mã hoá giao thức [HTTPS]

  • Vì authentication sử dụng HTTP header để truyền các thông tin xác thực [dữ liệu nhạy cảm như: password, access token, ...], các kết nối này cần phải được mã hoá nếu không trong trường hợp các hacker có thể hack vào mạng WiFi của người dùng, những thông tin này có thể bị đánh cắp/

Không dùng URL query params để truyền dữ liệu nhạy cảm

  • URL và URL params [url params: thí dụ như //example.com/?password=123456] có thể được lưu [dưới dạng log] ở server, trình duyệt [trong history] -> có thể bị đánh cắp và lợi dụng.
  • Nếu bạn để authen token ở URL query param, nhiều user ngây thơ có thể copy url trên trình duyệt và send thẳng cho "hacker".
  • Kích thước URL thường bị giới hạn ở browser hoặc server, vì vậy sẽ không thể đảm bảo tính toàn vẹn của dữ liệu được gửi đi.

Ngăn chặn tấn công "brute-force"

  • "Brute-force" là phương thức tấn công kiểu "thử sai", thí dụ hacker sẽ thử đăng nhập bằng hàng loạt mật khẩu cho tới khi thành công [thường được thực hiện bằng tool].
  • Có thể ngăn chặn bằng cách triển khai một middleware "rate limit" ở phía backend, hầu như mọi ngôn ngữ / web framework hiện nay đều có hỗ trợ implement phần này.
  • Chặn IP một user nếu user này cố tình tìm kiếm lỗ hổng trên server [user này thường sẽ tạo ra các lỗi HTTP code 3xx, 4xx và 5xx], chặn luôn để tránh hậu hoạ về sau
    .
  • Đừng có để cho người ta biết là bạn dùng code gì ở backend [nó sẽ dễ tìm ra lỗ hổng hơn đó
    ], thường là xoá đi phần X-Powered-By trong response header [đặc biệt là nếu xài các framework của .NET và Java thường có sẵn phần này].

Update dependency trong code thường xuyên

  • Nên cập nhật thường xuyên các dependency, thư viện hoặc framework mà bạn xài trong code, thường các bản cập nhật sẽ fix các lỗi về bảo mật được phát hiện.
  • Các kiểm tra và update dependency nếu bạn xài NodeJS [cả server-side lẫn client-side] như sau:
# Hiển thị list các lib bị outdated npm audit # Update minor và patch version trong package.json yarn outdated yarn update # Update dependency theo minor và patch trong packjage.js yarn upgrade-interactive # Update lên bản mới nhất yarn upgrade-interactive --latest # Nếu xài NPM thì cũng tương tự npm outdated npm update # Có thể xài tools này để check kĩ hơn: npm-check-updates npm install -g npm-check-updates ncu
  • Ngoài ra, update phiển bản OS ở server thường xuyên [lên bản LTS mới nhất], nếu bạn không xài PaaS [như Google App Engine hoặc Heroku].

Monitor server thường xuyên

  • Triển khai monitor, logging trên server để biết trước các thay đổi bất thường trước khi xảy ra sự cố.

Cơ chế authentication

Có 2 cơ chế authentication chính [chúng ta sẽ đưa ra ưu nhược và so sánh sau] để xác thực user trong một hệ thống REST API.

  • Bearer Token
  • Authentication cookie

Bearer Token

Bearer Token là gì?

  • Bearer token là một giá trị nằm trong phần Authorization header của mỗi HTTP request. Nó mặc định không tự được lưu ở bất cứ đâu [không như cookie], bạn phảu quyết định nơi lưu nó. Ngoài ra nó không có thời gian hết hạn và không có associated domain [như cookie], nó chỉ là một chuỗi giá trị:
GET //www.example.com/api/users Authorization: Bearer my_bearer_token_value

Để xây dựng một ứng dụng stateless, chúng ta có thể dùng JWT để triển khai Bearer Token. Về cơ bản, JWT [JSON Web Token] có 3 phần:

  • Header
  • Payload [chứa các mô tả về user, thường là chứa user id và quyền của user đó: member hoặc admin + thời gian hết hạn của token]
  • Signature [chữ kí]

JWT là một chuẩn mở cryptographically secure định nghĩa cách truyền thông tin xác thực một cách stateless giữa 2 nơi dưới dạng JSON. Stateless nghĩa là ở phía server không cần lưu lại state của token này, phần thông tin của user được đóng thẳng vào token. Chuỗi JWT được encode bằng Base64. Phần signature của JWT là một chuỗi được mã hoá bởi header, payload cùng một secrect key [mã bí mật]. Do chính bản thân signature đã bảo gồm cả header và payload nên signature có thể được dùng để kiểm tra tính toàn vẹn của dữ liệu khi truyền tải [giống MD5 checksum].

Về cơ bản thì, client sẽ nhận được JWT token một khi đã được công nhận xác thực [authentication] bằng một user/password [hoặc một số phương pháp khác].

Sau khi đã authentication thành công và client giữ token, mỗi request tiếp theo của client sẽ đính kèm token này vào request header. Server khi nhận được request với token sẽ kiểm tra signature có hợp lệ không, nếu hợp lệ server sẽ dùng phần payload của token để truy xuất expire time và thông tin user [tuỳ nhu cầu].

Use case cơ bản

  • Gửi và nhận các kết nối cần xác thực giữa trình duyệt [browser] và server backend.
  • Gửi và nhận các kết nối cần xác thực giữa ứng dụng di động [mobile app], ứng dụng desktop và server backend.
  • Gửi và nhận các kết nối cần xác thực giữa server với server [M2M] của các tổ chức khác nhau [OpenId Connect là một ví dụ].

Lưu JWT ở đâu?

Nhắc lại lần nữa, JWT [và các bearer token] không tự động được lưu lại trên client [trình duyệt, app], mà chúng ta phải tự implement việc lưu nó ở đâu [RAM, local/session storage, cookie, etc...].

Việc lưu JWT ở local storage trên browser không được khuyến khích:

  • Khi user tắt trình duyệt thì JWT còn đó và có thể được dùng tiếp vào lần tiếp theo cho tới khi hết hạn.
  • Mọi đoạn JavaScript trên trang của bạn đều có thể truy cập vào local storage: không có gì bảo vệ cả.
  • Nó có thể được dùng bởi web worker.

Lưu JWT token ở session cookie có thể là giải pháp tốt, chúng ta sẽ nói tiếp về vấn đề này sau.

Xem thêm chi tiết về store token trong tài liệu của auth0.com: //auth0.com/docs/security/store-tokens

Các kiểu attack cơ bản

  • Cross-Site Scripting [XSS] là phương thức tấn công cơ bản nhất mà bạn phải quan tâm khi code JavaScript: Hacker sẽ bằng một cách nào đó [thao túng các JS dependency hoặc dùng user input để add các đoạn malicious javascript code] để trộm JWT của user, sau đó mạo danh họ.
  • Thí dụ, ở phần comment của blog, một user có thể thêm một comment với mã JavaScript để làm gì đó trên trang này [các user khác sẽ phải load phần JS của user này]:
.
  • Permanent cookies: thay vì bị xoá đi khi tắt trình duyệt, permanent cookie hết hạn vào một thời gian được chỉ định [Expires] hoặc sau một khoảng thời gian nhất định [Max-Age].
  • Ngoài ra, cookie được tạo bởi server [HTTP Response Header] có thể có một số tuỳ chọn:

    • HttpOnly cookie: Javascript ở browser sẽ không bao giờ đọc được những cookie này.
    • Secure* cookie: browser sẽ chỉ đính kèm cookie này vào request khi request đó được thực hiện thông qua giao thức mã hoá [thường là HTTPS].
    • SameSite cookie: cho phép server yêu cầu một cookie sẽ không được gửi đi với cross-site requests, phần nào đó bảo vệ khỏi các cuộc tấn công cross-site request forgery [CSRF]. SameSite chỉ mới là bản thử nghiệm và chưa được hỗ trợ bởi tất cả trình duyệt.

    Use case cơ bản

    • Gửi và nhận các kết nối cần xác thực giữa trình duyệt [browser] và server backend.
    • Nếu phát triển front-end là mobile app hoặc desktop app thì việc authentication với cookie sẽ khó hơn so với dùng JWT.

    Lưu cookie ở đâu?

    Cookie được lưu tự động bởi trình duyệt và có sẵn thời gian hết hạn [tuỳ trường hợp] vả cả associated domain.

    Các kiểu attack cơ bản

    • Cross-Site Scripting [XSS]: tương tự như với JWT Bearer Token nếu cookie không được tạo ra với HttpOnly option, hạcker có thể đánh cắp cookie này và giả mạo user để đánh cắp thông tin hoặc thực hiện giao dịch bất hợp pháp.
    • Cross-Site Request Forgery [CSRF] là một phương thức attack khá phổ biến với những trang authentication bằng cookie. Cấu hình CORS [Cross-Origin Resource Sharing] có thể được thực hiện trên server để giới hạn các hostname được gửi request tới. Tuy nhiên, CORS được kiểm tra ở phía client bằng trình duyệt. Tệ hơn, CORS chỉ có thể giới hạn request được thực hiện bằng các ngôn ngữ phía browser [JavaScript hoặc WSM], có nghĩa là nếu bạn gửi request qua form [HTML Form], CORS sẽ không thể kiểm tra, kiểu như thế này:

    Bởi vì không có đoạn JavaScript nào liên quan tới request được tạo ra bởi form này, CORS bị vô hiệu hoá và cookie sẽ được gửi qua request theo form này

    .

    Một ví dụ khác về attack bằng CRSRF: giả sử user đang đăng nhập ở facebook, truy cập một trang tên bad.com. Trang bad.com này đã bị kiểm soát bởi hackers và có một đoạn code như sau trong trang:

    Chủ Đề