JSON Web Tokens (JWT) vs Sessions

JWT là gì ?

Về bản chất, JWT là một dữ liệu chữ kí dưới dạng JSON. Bởi vì nó được "kí" nên phía nhận có thể xác minh tính xác thực của nó. Dung lượng của nó rất nhỏ vì nó là JSON.

JSON Web Token (JWT) là một chuẩn mở (RFC 7519) được định nghĩa một các ngắn gọn và gọn gáng - khép kín để truyền tải thông tin một cách an toàn giữa các bên như một JSON Objects. Thông tin này có thể được verify và đáng tin cậu bởi vì nó được kí một cách số hoá. JWTs có thể được kí sử dụng một secret ( với thuật toán HMAC) or một cặp public/private key sử dụng RSA.

Dữ liệu chữ kí ko có gì gọi là mới - cái hấp dẫn chúng ta ở đây là cách mà JWT được sử dụng để tạo nên 1 services RESTful đúng nghĩa - mà ko cần sessions. Dưới đây là cách giải thích đơn giản từ phía thế giới chúng ta cho cách mà nó hoạt động : Tưởng tượng rằng bạn vừa mới trở về nước sau 1 chuyển du lịch nước ngoài. Bạn làm thủ tục nhập cảnh và bạn nói rằng - "Hãy cho tôi qua, tôi có quốc tịch ở đây". Tất cả đều đúng cả nhưng làm cách nào bạn chứng minh với hải quan là bạn nói chính xách ? Tất nhiên bạn sẽ phải sử dụng hộ chiếu để xác thực danh tính của bạn. Hãy giả sử rằng tất cả các nhân viên cục xuất nhập cảnh có đủ thẩm quyền để khẳng định hộ chiếu của bạn là chính xác và được phát hành bởi văn phòng Hộ chiếu trong nước bạn. Hộ chiếu của bạn được xác thực và họ sẽ cho bạn qua.

OK, bây giờ hãy xem xét lại câu chuyện trên từ góc nhìn của JWT để xem ai là ai :p

  • Văn phòng cấp họ chiếu - một service xác thực sẽ issue ra JWT
  • Hộ chiếu - JWT của bạn được sign bởi Văn phòng hộ chiếu. Định danh của bạn sẽ có thể được đọc bởi bất kì ai nhưng chỉ có 1 số mới có thể verify được nó là thật hay giả
  • Quốc tịch - yêu cầu chứa trong JWT của bạn ( hộ chiếu )
  • Biên giới ( nơi làm thủ tục nhập cảnh ) - một lớp bảo mật trong app của bạn để xác minh JWT token trước khi grant access cho bạn tới các tài nguyên bên trong - trong trường hợp này là cho bạn vào nước
  • Đất nước - nguồn tài nguyên bạn muốn access vào ( ví dụ các APIs)

alt

In authentication, when the user successfully logs in using his credentials, a JSON Web Token will be returned and must be saved locally (typically in local storage, but cookies can be also used), instead of the traditional approach of creating a session in the server and returning a cookie.

Whenever the user wants to access a protected route, it should send the JWT, typically in the Authorization header using the Bearer schema. Therefore, the content of the header should look like the following:

NO SESSIONS !

Bạn không cần phải giữ các session data trên server để authen user. Các công việc cần làm sẽ chỉ như sau :

User gọi service authen, thường sẽ là gửi username và password. Authen service sẽ trả về chuối JWT - báo cho user biết họ là ai User sẽ request access tới các service đc bảo vệ bên trong bằng cách gửi lại token đó. Tầng Security sẽ check đảm bảo chữ kí trong token có chính xác hay ko để được grant access.

No sessions storage

Ko sử dụng session đòng nghĩa với việc ko cần storage lưu sessions. Nghe qua thì có vẻ ko có gì ấn tượng nhưng việc này thực sự đáng để so sanh một khi ứng dụng của bạn scale ngang. VÍ dụ , nếu bạn chạy một application trên nhiều server, thì việc sử dụng sessions sẽ là một gánh năng. Bạn hoặc sẽ phải cần một server để lưu session hoặc phải share chung disk space trên các con server đó or phải sử dụng sticky session trên load balancer. Không một việc làm nào vừa kể trên là cần thiết nếu bạn không sử dụng sessions để authen.

Không cần xử lý các session rác

Thông thường, các sessions sẽ có thời hạn hết hạn và cần phải được xử lý kiểu xoá đi các sesssions "rác". JWT hoàn toàn có thể sở hữu chính expiry date của chính nó kèm với dữ liễu user. Cho nên khi tầng Security check authen của JWT, nó có thể check expiry time của token và đơn giản là từ chối truy cập.

Truly RESTful services

Chỉ khi ko sử dụng sessions thì bạn mới có thể tạo nên 1 service thuần RESTful, bởi vì một service thuần RESTful được định nghĩa là phải stateless. Với dung lượng nhỏ, JWT có thể được gửi lên với mọi request cũng giống như session cookie. Nhưng ko giống với session cookie, nó ko cần phải trỏ đến bất kì dữ liệu nào được lưu trữ trên server, JWT đã có dữ liệu.

JWT trông như thế nào ?

Có một điều chúng ta cần phải làm rõ trước khi đi sâu hơn. JWT là gì ? trông như thế nào ? Nó hăojc là một dạng dữ liệu của JSON Web Signature (JWS) hoặc là một JSON Web Encryption.

Token trong JWT được mã hoá thành một object JSON và đc sử dụng như là một payload của cấu trúc JWS hoặc là một plaintexxt của cấu trức JWE. Vế trước cho chúng ta chỉ một chữ kí và một dữ liệu mà chữ kí đó chứa có thể đọc được bởi bất kì ai. Vế sau cho ta một cơ chế mã hoá, nơi mà chỉ có một người với chìa khoá có thể giải mã nó.

Bên trong JWT/JWS

  • Header : các thông tin về thuật toán signing, type của payload (JWT) và format JSON
  • Payload : các dữ liệu thực tế trong JSON format
  • Signature : chữ kí

Mỗi thành phần đc nếu trên (header, payload, signature) được encode base64url, sau đó được dán lại với nahu với một "chấm" để tạo nên JWT. Dứoi đây là ví dụ :

var header = {  
        // Thuật toán signing.
        "alg": "HS256",
        // Thuộc tính type  "JWT"
        "typ": "JWT"
    },
    // Object header Base64 
    headerB64 = btoa(JSON.stringify(header)),
    // The payload 
    payload = {
        "name": "John Doe",
        "admin": true
    },
    // Payload Object Base64
    payloadB64 = btoa(JSON.stringify(payload)),
    // signature
     signature = signatureCreatingFunction(headerB64 + '.' + payloadB64),
    // signature Base64 
    signatureB64 = btoa(signature),
    // Đính tất cả các thành phần ở trên với dầu chấm để tạo nên JWS
    jwt = headerB64 + '.' + payloadB64 + '.' + signatureB64;

Kết quả của một JWS sẽ nhìn như dưới đây : Khả đẹp và gọn gàng phải ko 😃

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.OLvs36KmqB9cmsUrMpUutfhV52_iSz4bQMYJjkI_TLQ  

Một điều khá quan trọng là các chữ kí được tính toàn bới cả header và payload trong một lần gửi request. Do đó tính xác thực của header và payload cũng có thể được kiểm tra dễ dàng nhu sau :

[headerB64, payloadB64, signatureB64] = jwt.split('.');

if (atob(signatureB64) === signatureCreatingFunction(headerB64 + '.' + payloadB64) {  
    // good
} else
    // no good
}

Những cái gì có thể cho vào JWT header

JWT header được gọi là JOSE header. JOSE là viết tắt của JSON Object Signing and Encryption. Như bạn mong đợi, cả JWS và JWE đều có header như vậy, tuy nhiên mỗi cái có một bộ parameter hơi khác nhau. Dưới đây là danh sách các parameter được đăng kí cho JWS. Tất cả ngoài cái thứ nhất (alg) là optional :

  • alg Algorithm (compulsory)
  • typ Type (for JWT it has a value JWT, if present)
  • kid Key ID
  • cty Content Type
  • jku JWK Set URL
  • jwk JSON Web Key
  • x5u X.509 URL
  • x5c X.509 Certificate Chain
  • x5t X.509 Certificate SHA-1 Thumbprint
  • x5t#S256 X.509 Certificate SHA-256 Thumbprint
  • crit Critical

2 cái đầu khá được dùng phổ biến, do đó header phổ biến sẽ trông như sau :

{
    "alg": "HS256",
    "typ": "JWT"
}

parameter thứ 3 được liệt kê ở trên là kid, khá tiện vì lý do security. cty cũng nên được sử dụng khi thực hiện các giao dịch với nested JWTs. Phần còn lại bạn có thê tự Google tìm hiểu.

JWT claim những gì ?

Mình gặp vấn đề khi chọn từ tiếng việt hợp lý cho từ "claim" trong bài dịch này... Không biết chọn từ gì cho chuẩn. Nói chung thì "claims" nó là "phần thịt" của một JWT - đó là một dữ liệu mà chúng ta quan tâm focus rất nhiều để kí tên. Ví dụ, client "claim" username và user roles hoặc bất kì thứ gì mà nó sẽ grant cho client quyền access vào các tài nguyên sau đó.

Còn nhớ câu chuyện tôi đã lấy ví dụ ở đầu bài viết ? Đất nước của bạn là một claim và hộ chiếu của bạn chính là JWT.

Bạn có thể đặt bất kì thứ gì bạn muốn trong claim, tuy nhiên có một danh sách đã được đăng kí, cái mà đã được thừa nhận trên toàn thế giới trong quá trình implement JWT. Hãy chú ý list dưới :

  • exp - Expiration Time
  • nbf - Not Before
  • iat - Issued AtHow to use JWT in my application?
  • sub - Subject
  • iss - Issuer
  • aud - Audience
  • jti - JWT ID

Implement JWT

Trong hầu hết các kịch bản, client ở phía browser sẽ xác thực trong service authen và nhận JWT trong response. Sau đó, client lưu token bằng 1 cách nào đó ( bộ nhớ, localStorage v.v... ) và send lại trong mọi request với một nguồn tài nguyên đc bảo vệ. Thông thường token đc sent như là cookie hoặc header Authorization trong HTTP request :

GET /api/secured-resource HTTP/1.1  
Host: example.com  
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.OLvs36KmqB9cmsUrMpUutfhV52_iSz4bQMYJjkI_TLQ  

Header được prefer bởi vì lí do reason - cookies có thể bị bắt được bởi CSRF (Cross Site Request Forgery) ( trừ khi CSRF tokens đã được dùng ) . Lý do thứ 2 là cookie chỉ có thể được gửi ngược lại đối với cùng 1 tên miền ( or là tên miền cáp 2 ) nơi mà nó được issue. Nên nếu 1 một service authen mà cung cấp cho các domain khác nhau thì cách sử dụng cookies sẽ phải yêu cầu khá lich hoặt.

Logout với JWT?

Vì ko có sessions được lưu trên service, logout ko cần thiết phải tiến hành bằng cách destroy session. Do đó đăng xuất là trách nhiệm của client.

Tại sao nên dùng JST

  • Dễ dàng scale up
  • Dễ dàng maintain và debug
  • Thuần RESTful Services
  • Chức năng expiration built-in .
  • JSON Web Tokens tự bản thân đã chứa data.

JWTs vs. Sessions

Trước khi chúng ta đi sâu hơn nữa, lại một lần nữa lướt lại flow làm việc của 2 hệ thống này. Sơ đồ dưới đây sẽ cho thấy sự khác biệt của 2 mô hình

Authen bằng Cookie

Authen dựa trên Cookie dựa trên các phương thức default. tried-and-true để xử lý việc authen trong một thời dài. Đây là phương thức stateful. Có nghĩa là một record authen ( có thể hiểu là session) sẽ phải được giữ cả 2 phía client và server. Server cần theo dõi active session trong DB, trong khi đó cookie được tạo ở phía front-end và giữ 1 session định danh. Hyax xem qua flow xử lý truyền thống của phương pháp này :

  1. User nhập thông tin đăng nhập
  2. Server xác minh các thông tin đó là chính xác và tạo 1 session lưu trong DB
  3. Cookie với session ID sẽ được lưu ở browser
  4. Các request tiếp theo, session ID sẽ được xác minh tại DB và nếu nó hợp lí, request sẽ được xư lý để truy cập các tài nguyên sâu hơn đc bảo vệ
  5. Khi user log out khỏi ứng dụng, session sẽ bị destroy ở cả 2 phía client và server.

Authen bằng token

Phương thức authen này được sử dụng khá nhiều trong vài năm qua do sự gia tăng của các ứng dụng sử dụng một trang duy nhất, web APIs, IoT. Phương thức này là stateless. Server ko cần phải giữ bất kì record nào thê hiện user đã log in hay chưa ? Thay vì thế mọi request đều được tới server đều được đính kèm một token - cái này sẽ được server sử dụng để xác thực authen của request đó.

  1. User nhập thông tin đăng nhập
  2. Server xác minh thông tin đăng nhập chuẩn ko ? trả về token trong response
  3. Token lưu ở client - thương sẽ lưu ở storage local nhưng có thể lưu trong sessions storage or cookie.
  4. Các request tiếp theo đc gửi lên server sẽ chứa token , thường sẽ cho vào header Authorization
  5. Server decode JWTs và nếu token hợp lệ thì request đó sẽ đc pass qua authen
  6. Khi user log out, token sẽ bị destroy ở phía client, ko cần tương tác với bên server.

Stateless, Scalable & Decoupled

Lợi thế lớn nhất của sử dụng token là stateless. Back-end ko cần phải lưu thông tin theo dõi tracking token. Mỗi token bản thân nó khép kín, chứa mọi thông tin đủ để yêu cầu xác thực tính hợp lệ cũng như truyền tải thông tin user thông qua claim của nó.

Server chỉ việc làm 2 việc, trả token một request login thành công và xác nhận các request có chứa token là hợp lệ hay ko ? Thực tế là các server cũng ko cần issue token nữa. Các service thứ 3 như Auth0 có thể xử lý việc này bằng cách issue ra các token này và server chỉ làm một việc duy nhất là check tính hợp lệ của các token.

Cross Domain và CORS

Cookie làm việc tốt với các domain; sub-domain đơn, nhưng khi cần quản lý cookie thông qua nhiều domain, nhưng khi cần phải quản lý cookie thông qua nhiều domain khác nhau thì nó trở nên rất khó khăn. Đối lập với việc đó, phương thức authen token với việc enable Cross-Origin Resource Sharing (CORS) sẽ cho phép APIs trên nhiều services khác nhau và domains khác nhau.

Lưu DATA trong JWT

Với cách tiếp cận của cookie, bạn sẽ lưu session_id trong cookie. Trái với việc đó, JWT cho phép bạn lưu bất kì loại metadata nào, miễn là nó phải là JSON 😄

Perfomance

Khi sử dụng cookie, back-end phải search session id tương ứng với nó. Cho dù bạn dùng SQL truyền thống or NoSQL thì khi so sánh với token, phương thức cookie lúc nào cũng lâu hơn. Thêm nữa, khi bạn khi bạn muốn lưu thêm thông tin user trong JWT, ví dụ user_role, bạn có thể lưu các thông tin dễ dàng trong data của JWT và gọi ra khi xử lý các dữ liệu trong request.

Ví dụ , bạn có 1 APIs api/orders nhận thông tin order cuối cùng từ ứng dụng, nưng chỉ user admin mới có quyền view data này. Nếu sử dụng cookie, khi request đc tạo, bạn sẽ phải vào DB để xác minh xem session id có hơp lý ko ? sau đó get thông tin user và xác minh rằng user đó role là admin, cuối cùng là get data orders. Với token, bạn chỉ cần lưu role của user đó trong JWT, khi có request lên và JWT được xác thực, bạn có thể gọi thẳng vào DB để lấy thông tin orders.

Mobile

Modern APIs ko thường xuyên tương tác với trình duyệt - browser. Viết một API sử dụng token có thể phục vụ cả browser và plateform native mobile như iOS or Android. Native mobile platforms và cookie hoàn toàn ko thể kết hơp đc với nhau. Ngoài ra có rất nhiều giới hạn và cân nhắc khi sử dụng cookies với platform điện thoại. Trong khi đó, Tokens cực kì dễ dàng để implement trên cả iOS và ANdroid. Chưa nói đến IoT - xu hướng công nghệ đang hot ầm ầm 2 năm trở lại đây hoàn toàn ko có khái niệm cookie ....

Một vài câu hỏi chung

JWT Size

Nhược điểm lớn nhất của JWT là size của nó. Một session - cookie khá nhỏ khi so sanh với kể cả những JWT nhỏ nhất. Tuỳ theo bạn định làm gì, size của token có thể là một vấn đề lớn nếu bạn muốn add nhiều dữ liệu vào trong đó.

Token được lưu ở đâu

Bạn được quyền chọn nơi lưu JWT. Thường thì nó sẽ được lưu ở local storage của browser và nó ổn trong hầu hết các cases. Có một vấn đề trong việc lưu JWT trong storage local cần phải được nhận thức được. Thứ 1 ko như cookies, local storage được cô lập tới các domain cụ thể và data ko thể access bởi các domain khác trừ khi chưa sub-domains. Bạn có thể lưu token trong cookies, với max size của cookie là 4kb, nó có thể là vấn đề nếu bạn sử dụng quá nhiều dữ liệu cho vào token. Ngoài ra, bạn có thể lưu token trong session storage - cách này khá tương tự với local storage nhưng nó sẽ bị closed nếu user closed browsers.

XSS và XSRF

Quay lại vấn đề về bảo mật. Một trong các mối quan tâm số 1 khi developers quyết định nên sử dụng token hay session là vấn đề bảo mật. 2 kiểu tấn công phổ biến nhất là Cross Site Scripting (XSS) và Cross SIte Request Forgery ( XSRF or CSRF ).

Tấn công Cross Site Scripting xảy ra khi một thực thể bên ngoài có thể thực thi code trong web của bạn. Kiểu tấn công này thường là sẽ xảy ra nếu website của bạn cho phép input các thông tin ko hợp lệ Nếu người tấn công có thể thực thi code trên domain của bạn, JWT của bạn sẽ ko an toàn. Tất nhiên hiện tại, đối phó với tấn công XSS đã khá dễ dàng hơn nhiều nếu so với tấn công XSRF. Rất nhiều frameworks ( bao gồm Angular) có thể tự động loại bỏ - làm sạch - ngăn chặn các mã độc.

Tấn công Cross Site Request Forgery ko phải là vấn đề nếu bạn sử dụng JWT với local storage. Ngược lại, nếu bạn cần phải lưu JWT ở cookie, bạn sẽ cần phải phòng chống XSRF. Giải thích XSRF khá mất thời gian. Để đơn giản hoá mọi thứ mình sẽ chỉ nói về cách phòng tránh kiểu tấn công này. Server, ngay sau khi thiêt lập một session với client sẽ chỉ tạo ra 1 token duy nhất. Saud đó, bất kì data nào đc gửi tới server, thì token này sẽ được gửi thông qua một hidden input field và server sẽ check liệu token có hợp lệ ko. Nói chung thì tốt nhất nên lưu JWT ở local storage, bạn sẽ ko cần lo về XSRF.

Ngoài ra, mọt trong những cách bảo vệ user và server là giảm time expiration của token. Bằng cách đó, kể cả khi một token đã bị bắt được , nó sẽ nhanh chóng trở thành vô dụng. Thêm nữa, bạn có thể tạo được các blacklist của các token bị vô hiệu hoá và ko cho các token này access vào hệ thống. Trong trường hợp xấu nhất, hãy thay đổi thuật toán gen token của bạn và focus tất cả các user phải login lại.

Tham khảo

  1. https://float-middle.com/json-web-tokens-jwt-vs-sessions/

  2. https://ponyfoo.com/articles/json-web-tokens-vs-session-cookies

  3. https://auth0.com/blog/cookies-vs-tokens-definitive-guide/