JWT Decode ≠ Verify | Khi nào chỉ đọc payload, khi nào bắt buộc kiểm chữ ký
Decode ≠ Verify. Decode chỉ đọc Header/Payload (Base64URL → JSON). Verify chữ ký mới chứng minh token chưa bị sửa và do issuer tin cậy phát hành. Debug 401: decode local trên trình duyệt; cấp quyền: luôn verify trên server. Đừng tin role: "admin" chỉ vì đã decode được — và đừng dán Bearer production lên site jwt lạ.
Team outsourcing hoặc freelancer Việt Nam hay gặp cảnh này: SPA React gọi API Nest/Laravel, staging trả 401, đồng nghiệp hỏi trên Zalo/Slack — ai đó dán nguyên Authorization: Bearer eyJ… vào jwt.io hoặc group chat. Payload hiện exp còn 20 phút, role: "admin" đẹp như demo. Kết luận sai phổ biến: “Token ổn rồi, chắc CORS hoặc axios lỗi” — trong khi vấn đề thật là chữ ký / aud / key rotation, và token production vừa nằm trong log bên thứ ba.
Bài này tách rõ khi nào chỉ đọc payload, khi nào bắt buộc verify, rủi ro tin claims chưa verify, và quy trình debug không làm lộ credential.
Bài viết này giúp bạn
- Phân biệt encode vs encrypt, decode vs verify
- Đọc cấu trúc JWT và bảng claims hay gặp (
exp,sub,aud,iss,alg) - Biết khi nào decode đủ, khi nào verify bắt buộc
- Tránh 4 bẫy: tin payload, dán prod token,
alg=none, nhầm refresh/access - Checklist debug 401 + case study kiểu freelance / outsourcing VN
- Dùng JWT Decode Kawa (local trình duyệt) đúng mục đích
JWT là gì (30 giây)
JWT (JSON Web Token) là chuỗi ba phần nối bằng dấu chấm:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyXzhmM2EifQ.SIGNATURE
│ Header │ Payload │ Signature │
| Phần | Decode cho thấy gì | Decode không làm được |
|---|---|---|
| Header | alg, typ, kid | Chứng minh alg không bị giả |
| Payload | exp, sub, aud, claim tùy chỉnh | Xác nhận chữ ký hợp lệ |
| Signature | Chuỗi opaque | Tính lại nếu không có secret/key |
Ví dụ payload sau decode:
{
"sub": "user_8f3a",
"exp": 1720000000,
"aud": "https://api.client-a.vn",
"iss": "https://login.client-a.vn",
"role": "admin"
}
exp là Unix giây (UTC). Đổi bằng new Date(exp * 1000). Nhầm timezone (“mai mới hết hạn” theo giờ VN) là nguồn hiểu nhầm kinh điển khi debug với khách overseas.
Payload JWT không mã hóa. Base64URL chỉ đổi dạng để truyền — ai cầm token đều đọc được. Không nhét mật khẩu, số thẻ, API key production vào claims. Bảo mật nằm ở chữ ký + TLS + thời hạn ngắn, không ở việc “giấu” JSON.
Decode vs Verify — ranh giới quyết định
| Hành động | Làm gì | Khi nào dùng |
|---|---|---|
| Decode | Base64URL → JSON Header/Payload | Debug: xem exp, aud, sub; viết ticket; học cấu trúc JWT |
| Verify | Tính lại chữ ký bằng secret hoặc public key | Mọi request API trước khi tin claims / cấp quyền |
| Authorize | Sau verify: kiểm tra role, scope, tenant | RBAC, multi-tenant, feature flag theo user |
Khi nào chỉ cần decode
- “Vì sao bị logout?” → xem
exp,nbf,iat - So sánh token local vs staging:
aud/isscó trùng env không - Viết bug report: “payload có
sub=…,exp=…” (đã che PII nếu cần) - Học / workshop: hiểu cấu trúc mà không đưa secret production vào máy học viên
Khi nào bắt buộc verify
- Mọi endpoint cần đăng nhập
- Quyết định admin / quyền ghi dữ liệu
- Trust bất kỳ claim nào từ client (kể cả
email_verified) - Gateway / BFF chuyển token xuống microservice
Frontend decode để hiển thị tên user thì được; frontend tự tin role rồi hiện trang admin mà không có verify server = lỗ hổng.
Rủi ro khi tin claims chưa verify
1. Payload đẹp ≠ token hợp lệ
Attacker (hoặc tester tò mò) có thể tự tạo JSON { "role": "admin" }, encode, gắn chữ ký rác. Nếu API chỉ parse payload hoặc cho phép alg: "none", request đi xuyên.
2. Algorithm confusion
API legacy chấp nhận cả HS256 và RS256. Attacker đổi header sang alg: none, hoặc dùng public key như secret HMAC. Decode vẫn ra JSON đẹp — chỉ verify đúng cấu hình (allowlist thuật toán) mới chặn.
3. Dán token lên Slack / Zalo / jwt.io
Access token còn hiệu lực = phiên đăng nhập. Log chatbot, ticket Jira public, hoặc decoder hosted = vector lộ. Coi như compromised: revoke refresh, bắt login lại.
4. Nhầm refresh và access
Refresh thường exp dài. Resource API kỳ vọng access ngắn. Decode “còn hạn” nhưng server reject vì sai loại token — không phải “decode thêm lần nữa”.
Bảng triệu chứng 401 thường gặp
| Triệu chứng | Xem trong payload (decode) | Kiểm tra phía server |
|---|---|---|
| 401 sau khi deploy | iss, aud còn khớp env mới? | JWKS URL, key rotation |
| Local OK, staging fail | Cookie domain / aud khác | Secret khác nhau theo env |
| ”Hết hạn” nhưng đồng hồ OK | exp UTC vs giờ máy | NTP / clock skew container |
Client thấy admin: true | Claim có trong schema đã ký không? | Không bao giờ tin claim chỉ từ client |
| Postman 200, SPA 401 | Header Bearer có gửi không? | Middleware auth + CORS riêng (đừng nhầm CORS với JWT) |
Quy trình debug an toàn (freelance / team VN)
- Tái hiện tối thiểu — Một
fetchlỗi trong Network. Copy đúng Bearer; tránh gửi full HAR chứa cookie khác. - Decode local — Dán vào JWT Decode (chạy trên trình duyệt, không upload server). Xác nhận
algkỳ vọng (không phảinonetrừ khi bạn cố ý cho phép ở dev). - Đọc claims hay lệch env —
aud,iss,kidsau khi rotate key. - Đối chiếu log API — Server nên log lý do verify fail (signature / exp / audience). Decode = client gửi gì; log = server từ chối vì sao.
- Nếu đã lộ — Rotate / revoke. Đừng chỉ “đợi exp”.
Verify HS256 trên tool — giới hạn
Công cụ JWT Decode Kawa cho phép kiểm chữ ký HS256 nếu bạn nhập secret — vẫn xử lý trong trình duyệt. Dùng cho token dev/test và secret không phải production.
- Production OIDC / Auth0 / Firebase thường dùng RS256 + JWKS → verify trên backend, không copy private key lên UI.
- Verify trên tool không thay middleware production (
jsonwebtoken,jose, passport, v.v.).
Case study (ẩn danh)
Case A — Zalo paste. Dev dán access token hỏi “sao 401?”. Token còn 15 phút. Ai có lịch sử chat đọc được = impersonate user. Fix quy trình: chỉ chia sẻ token đã hết hạn hoặc mock; decode local; screenshot payload đã che sub nếu cần.
Case B — Outsourcing SPA + API. Frontend Vercel (https://app.khachhang.com), API VPS. Local Vite proxy nên “JWT luôn ổn”. Staging dùng audience api-staging nhưng client vẫn gửi token audience api-dev. Decode thấy aud lệch trong 10 giây — trước đó team đổ lỗi axios interceptor.
Case C — Algorithm confusion. Legacy API nhận HS256. Sau khi “nâng cấp” RS256, quên tắt HS256. Tester gửi alg: none + payload admin. Decode đẹp; production phải allowlist alg cứng.
Checklist trước khi merge auth
- Mọi route bảo vệ đều verify chữ ký trên server
- Allowlist
alg(không tin header tùy ý) - Kiểm tra
exp(vànbfnếu có), cho phép clock skew nhỏ có chủ đích - Kiểm tra
aud/isstheo môi trường - Không nhét secret/PII vào payload
- Không dán Bearer production lên site / chat công khai
- Log lý do verify fail (không chỉ “Unauthorized”)
Cách dùng JWT Decode trên Kawa
- Mở Giải mã JWT
- Dán token (ưu tiên staging / đã hết hạn / mock)
- Đọc Header (
alg,kid) và Payload (exp,sub,aud,iss) - (Tuỳ chọn) Với HS256 + secret test: bấm xác minh chữ ký — chỉ để học / debug local
- Copy JSON claims vào ticket; không kèm full Bearer nếu ticket public
Công cụ phù hợp bước “nhìn claims” trong checklist trên — không thay API Gateway.
FAQ nhanh
Decode có chứng minh token hợp lệ không?
Không. Chỉ đọc nội dung đã encode.
Cần secret để decode không?
Không. Secret/key chỉ cần khi verify.
Frontend có nên verify JWT không?
Có thể decode để UX; quyền truy cập phải do server quyết sau verify. Verify HS256 trên trình duyệt với secret nhúng vào bundle = secret đã lộ.
Còn exp mà vẫn 401?
Xem aud, iss, key rotation, loại token (access vs refresh), clock skew — rồi đọc log server.
Liên kết liên quan
- Công cụ JWT Decode — decode Header/Payload local; HS256 verify tuỳ chọn với secret test
- JSON Formatter — làm đẹp body lỗi API sau khi đã qua auth
- Danh sách công cụ