TechDogy

(paduvi)

You can do anything, but not everything..

Khái niệm về JSON Web Token

Categories:

Thời gian qua, mình có phải vật lộn với 1 framework mới, mày mò trong tài liệu hướng dẫn thì thấy nó có đề cập tới 1 phương pháp xác thực quyền truy cập (Authentication) bằng JSON Web Token (JWT). Sau khi đào sâu hơn về cái này, mình nhận thấy quả thực JWT nó còn tuyệt vời hơn ngoài mong đời và khả năng của nó sẽ không chỉ dừng lại ở mỗi Authentication. Qua bài viết này mình muốn giúp những ai còn chưa biết tới JWT hoặc chưa hiểu rõ về nó hình dung được mô tả trực quan nhất về JWT và những gì mà JWT có thể đem lại cho Web Service.

JSON Web Token là gì?

JSON Web Token (JWT) là 1 tiêu chuẩn mở (RFC 7519) định nghĩa cách thức truyền tin an toàn giữa các thành viên bằng 1 đối tượng JSON. Thông tin này có thể được xác thực và đánh dấu tin cậy nhờ vào “chữ ký” của nó. Phần chữ ký của JWT sẽ được mã hóa lại bằng HMAC hoặc RSA.

Ví dụ cho 1 JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0.
yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw

Khi nào nên dùng JSON Web Token?

Dưới đây là 1 vài kịch bản thích hợp với JWT:

  • Authentication: Đây là kịch bản phổ biến nhất cho việc sử dụng JWT. Một khi người dùng đã đăng nhập vào hệ thống thì những request tiếp theo từ phía người dùng sẽ chứa thêm mã JWT, cho phép người dùng quyền truy cập vào các đường dẫn, dịch vụ, và tài nguyên mà cần phải có sự cho phép nếu có mã Token đó. Phương pháp này không bị ảnh hưởng bởi Cross-Origin Resource Sharing (CORS) do nó không sử dụng cookie.
Client application                                            API
    --------                                              -----------
        |                                                      |
        |                   GET /api/employees                 |
        |----------------------------------------------------->|
        |                     403 Forbidden                    |
        |<-----------------------------------------------------|
        |                                                      |
        |                                                      |
        |                 POST /api/authenticate               |
        |     { login: "paduvi", password: "chotoxautinh" }    |
        |----------------------------------------------------->|
        |                      200 Success                     |
        |             { token: "my.personal.token" }           |
        |<-----------------------------------------------------|
        |                                                      |
        |                                                      |
        |                 GET /api/employees                   |
        | Header { "Authorization: Bearer "my.personal.token" }|
        |----------------------------------------------------->|
        |                      200 Success                     |
        |<-----------------------------------------------------|
        |                                                      |
  • Trao đổi thông tin: JSON Web Token là 1 cách thức không tồi để truyền tin an toàn giữa các thành viên với nhau, nhờ vào phần “chữ ký” của nó. Phía người nhận có thể biết được  người gửi là ai thông qua phần chữ ký. Ngoài ra, chữ ký được tạo ra bằng việc kết hợp cả phần header, payload lại nên thông qua đó ta có thể xác nhận được chữ ký có bị giả mạo hay không.

Cấu trúc của JSON Web Token:

JSON Web Token bao gồm 3 phần, được ngăn cách nhau bởi dấu chấm (.):

  1. Header
  2. Payload
  3. Signature (chữ ký)

Tổng quát thì nó có dạng xxxxx.yyyyy.zzzzz. Hãy cùng nhau khám phá mỗi phần bên trong JWT nhé:

Header:

Phần Header dùng để khai báo kiểu chữ ký và thuật toán mã hóa sẽ dùng cho cái token của chúng ta.

Ví dụ cho phần Header:

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

Đoạn Header này khai báo rằng đối tượng được mã hóa là 1 JWT (để phân biệt với JWS hay JWE), và chữ ký của nó sử dụng thuật toán mã hóa HMAC SHA-256.

Đoạn Header này sẽ được mã hóa base64url, và ta thu được phần đầu tiên của JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Chú ý rằng mình viết ở phía trên là base64url chứ không phải là base64. Về cơ bản 2 cái này là tương tự nhau nhưng giữa chúng vẫn có những sự khác biệt:

  • Không thêm = vào
  • Các ký tự + và / sẽ được thay thế bằng  và _

Các bạn có thể so sánh sự khác biệt của chúng ở trang web encode online này:
http://kjur.github.io/jsjws/tool_b64uenc.html

Chúng ta có thể tự triển khai 1 hàm encode base64url do chính mình tạo ra. Dưới đây là code mô phỏng bằng Javascript:

function base64url(source) {
  // Encode in classical base64
  encodedSource = CryptoJS.enc.Base64.stringify(source);

  // Remove padding equal characters
  encodedSource = encodedSource.replace(/=+$/, '');

  // Replace characters according to base64url specifications
  encodedSource = encodedSource.replace(/\+/g, '-');
  encodedSource = encodedSource.replace(/\//g, '_');

  return encodedSource;
}

Ở đoạn code trên mình đã sử dụng thư viện CryptoJS để có thể mã hóa base64 rồi sau đó loại bỏ các ký tự = và thay thế các ký tự + / đi.

Để có thể sử dụng được hàm trên, đầu vào của bạn cần là 1 mảng byte ở định dạng UTF-8. Ta có thể chuyển đổi từ xâu ký tự sang mảng byte bằng 1 hàm khác cũng được cung cấp bởi CryptoJS:

var source = "Hello!";

// 48 65 6c 6c 6f 21
console.log(CryptoJS.enc.Utf8.parse(source).toString());

Cuối cùng ta đã thu được phần đầu tiên của JWT:

var header = {
  "alg": "HS256",
  "typ": "JWT"
};

var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header));
var encodedHeader = base64url(stringifiedHeader);

Payload (Claims):

Phần thứ 2 của token đó là Payload, nơi chứa các nội dung của thông tin (claim). Thông tin truyền đi có thể là mô tả của 1 thực thể (ví dụ như người dùng) hoặc cũng có thể là các thông tin bổ sung thêm cho phần Header. Nhìn chung, chúng được chia làm 3 loại: reserved, public và private.

  1. Reserved: là những thông tin đã được quy định ở trong IANA JSON Web Token Claims registry. Chúng bao gồm: Chú ý rằng các khóa của claim đều chỉ dài 3 ký tự vì mục đích giảm kích thước của Token
    • iss (issuer): tổ chức phát hành token
    • sub (subject): chủ đề của token
    • aud (audience): đối tượng sử dụng token
    • exp (expired time): thời điểm token sẽ hết hạn
    • nbf (not before time): token sẽ chưa hợp lệ trước thời điểm này
    • iat (issued at): thời điểm token được phát hành, tính theo UNIX time
    • jti: JWT ID
  2. Public: Khóa nên được quy định ở trong IANA JSON Web Token Registry hoặc là 1 URI có chứa không gian tên không bị trùng lặp.
    Ví dụ: "https://www.dogy.io/jwt_claims/is_admin": true
  3. Private: Phần thông tin thêm dùng để truyền qua giữa các máy thành viên.

Ví dụ cho phần Payload:

{
  "iss": "dogy",
  "exp": 1426420800,
  "https://www.dogy.io/jwt_claims/is_admin": true,
  "user": "admin",
  "awesome": true
}

Mã hóa base64url ta thu được phần thứ 2 của token:

eyJpc3MiOiJkb2d5IiwiZXhwIjoxNDI2NDIwODAwLCJodHRwczovL3d3dy5kb2d5LmlvL2p3dF9jbGFpbXMvaXNfYWRtaW4iOnRydWUsInVzZXIiOiJhZG1pbiIsImF3ZXNvbWUiOnRydWV9

Signature:

Phần chữ ký được tạo bằng cách kết hợp 2 phần Header + Payload, rồi mã hóa nó lại bằng 1 giải thuật encode nào đó, càng phức tạp thì càng tốt, ví dụ như HMAC SHA-256

var token = encodedHeader + "." + encodedData;

var secret = "My very confidential secret!";

var signature = CryptoJS.HmacSHA256(token, secret);
signature = base64url(signature);

Rồi ta sẽ thu được phần cuối của token:

ihV-2TdaUXitRvlMXmRJvquUnnp13BL1aMIK0tlai2w

Putting All Together:

Tổng kết lại, JWT gom lại từ ví dụ trên sẽ có kết quả là:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkb2d5IiwiZXhwIjoxNDI2NDIwODAwLCJodHRwczovL3d3dy5kb2d5LmlvL2p3dF9jbGFpbXMvaXNfYWRtaW4iOnRydWUsInVzZXIiOiJhZG1pbiIsImF3ZXNvbWUiOnRydWV9.ihV-2TdaUXitRvlMXmRJvquUnnp13BL1aMIK0tlai2w

Và đây là đoạn code Javascript triển khai toàn bộ công việc trên:

var header = {
  "alg": "HS256",
  "typ": "JWT"
};

var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header));
var encodedHeader = base64url(stringifiedHeader);

var data = {
  "iss": "dogy",
  "exp": 1426420800,
  "https://www.dogy.io/jwt_claims/is_admin": true,
  "user": "admin",
  "awesome": true
};

var stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data));
var encodedData = base64url(stringifiedData);

var token = encodedHeader + "." + encodedData;

var secret = "My very confidential secret!";

var signature = CryptoJS.HmacSHA256(token, secret);
signature = base64url(signature);

var signedToken = token + "." + signature;

Mình chỉ minh họa như vậy thôi, chứ không khuyến khích mọi người tự mất công làm lại tất cả các công đoạn vì hiện nay đã có rất nhiều thư viện hỗ trợ công việc này. Các bạn có thể tham khảo danh sách các thư viện và thử debug JWT  ở trên trang web https://jwt.io/.

Ví dụ đơn giản về Authentication bằng JWT

Code bên phía API Server, sử dụng Golang:

package main

import (
	"github.com/dgrijalva/jwt-go"
	jwtmiddleware "github.com/iris-contrib/middleware/jwt"
	"github.com/kataras/iris"
)

func main() {

	myJwtMiddleware := jwtmiddleware.New(jwtmiddleware.Config{
		ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
			return []byte("My Secret"), nil
		},
		SigningMethod: jwt.SigningMethodHS256,
	})

	iris.Get("/secured/ping", myJwtMiddleware.Serve, SecuredPingHandler)
	iris.Listen(":8080")

}

type Response struct {
	Text string `json:"text"`
}

func SecuredPingHandler(ctx *iris.Context) {
	response := Response{"All good. You only get this message if you're authenticated"}
	// get the *jwt.Token which contains user information using:
	// user:= myJwtMiddleware.Get(ctx) or context.Get("jwt").(*jwt.Token)
	ctx.JSON(iris.StatusOK, response)
}

Bây giờ nếu mình vào thử đường dẫn http://localhost:8080/secured/ping bằng trình duyệt ta sẽ thu được kết quả là:

Required authorization token not found

Đúng như dự kiến, truy cập vào đường dẫn không thành công do mình chưa khai báo token. Để có thể authenticate thành công, ta cần bổ sung thêm HTTP Header cho request phía client truy cập. Phía dưới là code minh họa bằng Node.js:

const
    url = 'http://localhost:8080/secured/ping',
    request = require('request'),
    jwt = require('jsonwebtoken'),
    payload = {
        user: 'paduvi',
        company: 'Techmaster'
    },
    secretKey = 'My Secret';

var token = jwt.sign(payload, secretKey, {algorithm: 'HS256', expiresIn: '1h'});

var callback = function (error, response, body) {
    if (error) {
        console.error(error);
    } else {
        console.log("Status Code: " + response.statusCode);
        console.log("Response Data: " + body);
    }
}

var options = {
    url: url,
    headers: {
        'Authorization': 'Bearer ' + token
    }
}

request(options, callback);
// or
request.get(url, {
    'auth': {
        'bearer': token
    }
}, callback);

Kết quả hiển thị trên console là:

Status Code: 200
Response Data: {"text":"All good. You only get this message if you're authenticated"}

Nếu như mình dùng secret key không hợp lệ, kết quả trả về sẽ là:

Status Code: 401
Response Data: signature is invalid

Hay bổ sung thêm Reserved Claim nbf (Not before time) với thời gian là khoảng vài phút sau thì kết quả là:

Status Code: 401
Response Data: Token is not valid yet

Ngoài ra, các bạn có thể chỉnh sửa lại code để test nốt với các trường hợp còn lại có thể xảy ra, ví dụ như exp (expired at), iat (issued at)…

Latest Comments:

  1. Mình đi search về storage engine thì thấy series về bài của bạn (mình đoán là bạn cũng đọc từ…

  2. Series rất hay, ủng hộ admin làm thêm về các database khác như Scylladb (discord mới migrate từ Cassandra sang)

  3. bài viết rất chất lượng, ủng hộ mạnh tác giả

  4. Mình đang làm về authentication thì phải tìm hiểu thêm về JWE (Json Web Encryption) và JWS (Json Web Signature).…

One response to “Khái niệm về JSON Web Token”

  1. Steve Avatar
    Steve

    Mình đang làm về authentication thì phải tìm hiểu thêm về JWE (Json Web Encryption) và JWS (Json Web Signature). Thật ra JWT chỉ có 2 phần là Header và Payload chứ không có chữ kí (signature). Phần signature này được thêm vào thì cái JWT sẽ được gọi là JWS.

    Nhưng JWS chỉ là nói về việc tamper-proof, là bạn sẽ verify được cái token này có đến từ đúng người gửi không nhờ vào signature nhưng không thể che giấu được các claims trong payload khi gửi qua các insecured mediums. Nên JWE + JWS hay còn gọi là nested JWTs sẽ là một lựa chọn tốt hơn nếu bạn muốn JWT của bạn có cả 2 tính chất là mã hoá dữ liệu + chống can thiệp.

Leave a Reply

Your email address will not be published. Required fields are marked *