Cách đọc JWT: header, payload, chữ ký và thời hạn

Tách một JSON Web Token thành ba phần, đọc exp, iat, nbf thành ngày giờ thật, hiểu vì sao giải mã không phải xác minh, và khác nhau giữa HS256 với RS256.

Trong bài này

JWT, viết tắt của JSON Web Token, là ba chuỗi Base64URL nối với nhau bằng dấu chấm: phần đầu (header), phần dữ liệu (payload) và chữ ký. Để đọc một JWT, hãy giải mã hai phần đầu, vốn là JSON thông thường, rồi đổi các claim thời gian exp, iat và nbf thành ngày giờ. Cách đó cho biết token nói gì và còn nằm trong thời hạn hay không. Nó không cho biết token có thật hay không: nội dung chỉ được mã hóa dạng biểu diễn chứ không được bảo vệ, và chỉ việc kiểm tra chữ ký mới chứng minh có người nắm khóa đã tạo ra chúng. Bài này đi qua một token thật từng phần, giải thích các claim thời gian, so sánh HS256 với RS256, và đưa lời khuyên thẳng thắn về việc dán token vào bất kỳ công cụ trực tuyến nào.

Ba phần của token

Đây là một token tạo riêng cho bài viết, với khóa ký là demo-secret. Nó được chia làm ba dòng để dễ đọc; token thật là một dòng với dấu chấm giữa các phần.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkxpbmggTmd1eWVuIiwiaWF0IjoxNzAwMDAwMDAwLCJuYmYiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0
.U6xZB-kxVEScuawWsaJimzYTOzDy1ZDS_N15xeZKxho

Header (phần đầu). Phần thứ nhất giải mã ra {"alg":"HS256","typ":"JWT"}. Nó cho biết token được ký thế nào (alg) và là loại gì (typ). Nó cũng có thể kèm kid, mã định danh của khóa cần dùng.

Payload (phần dữ liệu). Phần thứ hai giải mã ra:

{
  "sub": "1234567890",
  "name": "Linh Nguyen",
  "iat": 1700000000,
  "nbf": 1700000000,
  "exp": 1700003600
}

Đây là các claim. Một số là tên đã được chuẩn hóa với ý nghĩa cố định, một số do bên phát hành tự đặt, như name.

Chữ ký. Phần thứ ba không phải JSON. Đó là các byte thô của chữ ký tính trên hai phần đầu đúng như được viết, header.payload, rồi ghi bằng Base64URL. Với HS256 nó dài 32 byte, tức 43 ký tự Base64URL không đệm.

Bộ mã hóa là Base64URL, biến thể an toàn cho URL với - và _ và không có dấu = đệm. Bài về Base64 giải thích vì sao, và vì sao đây là cách biểu diễn chứ không phải mã hóa bảo mật.

Những claim bạn gặp nhiều nhất

ClaimÝ nghĩa
issBên phát hành: ai đã tạo token
subChủ thể: token nói về ai hoặc cái gì, thường là ID người dùng
audĐối tượng nhận: token dành cho ai
expThời điểm hết hạn: từ lúc này trở đi token phải bị từ chối
nbfKhông trước thời điểm: token không được chấp nhận trước lúc này
iatThời điểm phát hành: token được tạo khi nào
jtiMã định danh duy nhất của token này

Các tên này đến từ RFC 7519, chuẩn của JWT. Mọi thứ khác do ứng dụng tự quy định.

Đọc exp, iat và nbf

Ba claim thời gian chứa số giây tính từ 00:00:00 UTC ngày 1 tháng 1 năm 1970, mốc epoch của Unix. Với token ở trên:

ClaimGiá trịNgày giờ (UTC)
iat170000000014/11/2023, 22:13:20
nbf170000000014/11/2023, 22:13:20
exp170000360014/11/2023, 23:13:20

Vậy token sống đúng 3600 giây, tức một tiếng. Ngày nay đã qua exp từ lâu, nên bộ giải mã báo là hết hạn. Token "chưa có hiệu lực" nếu thời điểm hiện tại còn trước nbf, và nó hết hạn đúng lúc chạm exp, không phải chậm một giây.

Có ba chỗ hay sai với các mốc này.

  • Mili giây bị ghi như giây. Date.now() của JavaScript trả về mili giây. Token có exp được điền bằng giá trị đó, ví dụ 1700003600000, nằm ở tương lai hàng chục nghìn năm, và token không bao giờ hết hạn. Bộ giải mã tốt sẽ báo rằng giá trị không phải số giây hợp lý thay vì hiện một năm vô nghĩa. Bài về giây và mili giây chỉ cách phân biệt chúng.
  • Lệch đồng hồ. Hai máy chủ hiếm khi khớp nhau đến từng giây. Nhiều hệ thống cho phép độ lệch nhỏ, thường một hai phút, khi kiểm tra exp và nbf. Nếu một token "hết hạn trước khi được phát hành", hãy nghi đồng hồ.
  • Múi giờ. Các giá trị luôn là UTC. Bộ giải mã hiển thị thêm theo giờ địa phương của bạn, và công cụ đổi dấu thời gian làm điều tương tự cho mọi số epoch.

Giải mã không phải là xác minh

Đây là quy tắc quan trọng nhất về JWT. Ai có token cũng đọc được nó, và ai cũng viết được một token với nội dung tùy ý. Phần đầu {"alg":"none","typ":"JWT"} là chuỗi eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0, và token dùng nó hoàn toàn không có chữ ký. Thấy "admin": true trong payload đã giải mã tự nó chẳng chứng minh được gì.

Điều chứng minh token thật là kiểm tra chữ ký bằng đúng khóa, ở phía máy chủ, trong mọi yêu cầu. Hai quy tắc từ tài liệu thực hành tốt nhất RFC 8725 đáng nhớ:

  • Máy chủ tự quyết định thuật toán nào được chấp nhận. Không bao giờ tin trường alg của token để chọn cách kiểm tra, vì đó là con đường cho token không chữ ký và tấn công nhầm lẫn thuật toán lọt qua.
  • Một token chỉ đáng tin bằng các bước kiểm tra exp, aud và iss của nó, chứ không chỉ chữ ký.

Đừng đặt bí mật vào payload. Ai nhìn thấy token cũng đọc được, kể cả bộ nhớ cục bộ của trình duyệt, tệp nhật ký và ảnh chụp màn hình.

HS256 và RS256

Giá trị alg gọi tên phương pháp ký. Có hai phương pháp phổ biến hơn hẳn.

  • HS256 là HMAC với SHA-256. Một khóa bí mật dùng chung vừa ký vừa kiểm tra. Cách này đơn giản, nhưng ai kiểm tra được thì cũng làm giả được: mọi dịch vụ giữ khóa đều có thể tạo token. Các biến thể HS384 và HS512 dùng hàm băm dài hơn với cùng ý tưởng. Chữ ký của HS256 dài 32 byte, tức 43 ký tự.
  • RS256 là RSA với SHA-256. Khóa riêng để ký, khóa công khai để kiểm tra. Bên phát hành giữ khóa riêng, còn mọi dịch vụ có thể kiểm tra token bằng khóa công khai, thường được công bố dưới dạng một bộ khóa. Chữ ký RSA 2048 bit dài 256 byte, tức 342 ký tự. ES256 dùng đường cong elliptic cho cùng cách chia khóa riêng và khóa công khai, với chữ ký ngắn hơn.

Vì vậy chữ ký khoảng 43 ký tự gợi ý HS256 còn khoảng 342 ký tự gợi ý khóa RSA 2048 bit, dù chỉ trường alg ở header mới gọi tên chắc chắn.

Đọc token bằng công cụ giải mã

  1. Mở công cụ giải mã JWT và dán token. Chữ Bearer đứng đầu, dấu nháy và dấu xuống dòng được tự gỡ. "Ví dụ" nạp một mẫu.
  2. Đọc Phần đầu (header) và Phần dữ liệu (payload) dưới dạng JSON đã định dạng. Mục Thời điểm hiển thị exp, nbf và iat theo múi giờ của bạn, dạng ISO 8601 UTC và dạng tương đối, kèm một trạng thái: đã hết hạn, chưa có hiệu lực, còn hạn hoặc không có ngày hết hạn.
  3. Để kiểm tra chữ ký HS256, HS384 hoặc HS512, nhập khóa bí mật ở ô Khóa bí mật cho HS256, HS384, HS512. Với demo-secret, token ở trên hiện "Chữ ký hợp lệ"; với khóa khác nó báo chữ ký không khớp. Chọn "Khóa bí mật ở dạng Base64URL" nếu hệ thống của bạn lưu khóa theo dạng đó.
  4. Với RS256, ES256 và các thuật toán dùng khóa riêng khác, công cụ cho biết nó không kiểm tra được. Hãy dùng thư viện JWT của ngôn ngữ bạn với khóa công khai.

Nếu chuỗi có năm phần thay vì ba, đó là token được mã hóa (JWE) và không đọc được nếu thiếu khóa. Lỗi như "JWT có ba phần" thường nghĩa là một đoạn bị cắt mất khi sao chép.

Cẩn trọng với token thật

Công cụ giải mã chạy trong trình duyệt. Token và khóa được xử lý trên thiết bị của bạn và không được tải lên. Dù vậy, token là một thông tin xác thực: ai giữ một token còn hạn đều dùng được cho đến khi nó hết hạn. Vì thế:

  • Hãy ưu tiên token thử nghiệm, hoặc token đã hết hạn, khi bạn chỉ muốn hiểu cấu trúc.
  • Đừng dán token còn hạn vào chat, phiếu hỗ trợ hay ảnh chụp màn hình. Nếu một token đã lộ, hãy coi như đã rò rỉ: thu hồi nó hoặc chờ nó hết hạn, và cân nhắc rút ngắn thời hạn token.
  • Đừng dán khóa ký của môi trường production vào bất kỳ trang web nào, kể cả trang này. Với khóa HS256, ai biết được khóa là làm giả được token. Hãy kiểm tra chữ ký production trong mã của bạn hoặc trên máy của bạn.

Để đọc payload mà không cần trang web nào, hãy dùng terminal. Lệnh Node.js này in payload dưới dạng đối tượng:

node -e "console.log(JSON.parse(Buffer.from(process.argv[1].split('.')[1],'base64url')))" "$TOKEN"

Danh sách kiểm tra ngắn

Tách theo dấu chấm, giải mã hai phần đầu, và đọc exp, nbf, iat là số giây theo UTC. Nhớ rằng giải mã không chứng minh gì: chữ ký phải được xác minh ở máy chủ với danh sách thuật toán được chấp nhận cố định, và kiểm tra cả aud lẫn iss. Biết token là HS256 với khóa dùng chung hay RS256 với cặp khóa. Đừng dán token còn hạn hay khóa production vào nơi không cần thiết.

Công cụ được nhắc trong bài

Về trang blog