JWT & HMAC: Xác thực không Session và những giới hạn cần biết
JWT và HMAC hoạt động thế nào? Hướng dẫn HS256, claim validation, access/refresh token, lưu token, revoke, key rotation và bảo mật API với Node.js.
ShanverseCập nhật 11 phút đọc— lượt xemĐang tải lượt xem
JWT
Cryptography
Authentication
JWT giải quyết bài toán gì?
Sau khi user chứng minh danh tính bằng password, passkey hoặc Identity Provider, authorization server phát hành token chứa một tập claim. Resource server xác minh token trước khi cho phép truy cập API.
Mermaid
Loading diagram…
Với self-contained JWT, API có thể xác minh tính toàn vẹn và nguồn phát hành bằng key mà không cần lookup session cho mỗi request. Đây là stateless verification, không có nghĩa toàn bộ hệ thống authentication không có state.
JWT không tự “đăng nhập” user
JWT chỉ biểu diễn claims. Một hệ thống hoàn chỉnh còn cần:
Cơ chế login và xác minh credential.
Chính sách phát hành token.
Bình luận
Validation nghiêm ngặt ở resource server.
Authorization theo resource/tenant.
Rotation, revoke và incident response.
Transport và storage an toàn.
OAuth 2.x và OpenID Connect là protocol; JWT chỉ là một format có thể được protocol sử dụng. OAuth access token cũng có thể là opaque token. ID token dành cho client xác minh authentication event, không mặc nhiên là access token cho API.
Cấu trúc ba phần
JWT thường gặp dưới dạng JWS compact serialization:
Ví dụ đã rút gọn:
Header
alg: thuật toán bảo vệ token.
typ: explicit type để giảm token-type confusion.
kid: gợi ý server chọn key trong key ring; không phải key và không đáng tin trước verification.
Payload
Signature/MAC
Phần thứ ba chứng minh header và payload không bị sửa bởi bên không có key.
Nếu cần confidentiality, đánh giá JWE hoặc tốt hơn là chỉ đặt identifier tối thiểu trong token và giữ dữ liệu nhạy cảm ở server. Signed JWT/JWS chỉ cung cấp integrity và authenticity, không cung cấp secrecy.
HS256 và HMAC hoạt động thế nào?
HS256 nghĩa là HMAC dùng SHA-256.
HMAC kết hợp cryptographic hash với secret key theo construction chuẩn. Không thay bằng:
Các construction tự nối chuỗi không tương đương HMAC và có thể có lỗi thiết kế.
HMAC không phải digital signature theo nghĩa public-key
Với HS256, cùng một secret dùng để ký và verify. Mọi service có quyền verify cũng có thể tạo token hợp lệ.
Điều này có hệ quả:
Không có public key để phân phối tự do.
Khó giới hạn quyền “verify-only”.
Một resource server bị compromise có thể forge token cho các service dùng cùng secret.
HMAC không cung cấp non-repudiation giữa các bên cùng giữ secret.
HS256 phù hợp khi issuer và verifier nằm trong trust boundary nhỏ. Với nhiều microservice hoặc third-party verifier, RS256/PS256, ES256 hoặc EdDSA thường dễ phân tách quyền hơn: private key chỉ ở issuer, public key ở verifier.
Claims phải được validate, không chỉ signature
Một MAC hợp lệ chỉ nói rằng token được tạo bởi bên có secret và chưa bị sửa. API vẫn phải kiểm tra token có dành cho API này, mục đích này và thời điểm này hay không.
Dùng clock tolerance nhỏ cho clock skew, không biến tolerance thành cách kéo dài token đáng kể.
Algorithm confusion và alg: none
Không decode header rồi tự động chọn bất kỳ thuật toán nào token yêu cầu. Verification policy phải cố định ở server:
Cần:
Reject none.
Không trộn HS256 và RS256 trong cùng validation path nếu không có token profile/key type rõ ràng.
Một key chỉ dùng cho đúng algorithm/purpose đã định.
Dùng library JOSE được duy trì, không tự viết parser/crypto.
Nếu hệ thống mong RS256 nhưng nhầm dùng public key như HMAC secret cho token HS256, attacker có thể khai thác algorithm confusion ở implementation yếu.
Key cho HS256 phải là random secret mạnh
Không dùng:
Dùng ít nhất 256 bit random từ CSPRNG cho HS256, lưu trong secret manager/KMS-compatible system, giới hạn quyền đọc và audit access.
Không commit secret vào Git, .env.example, Docker image, frontend bundle, mobile app hoặc log.
Không chia sẻ một HMAC secret quá rộng
Mỗi issuer/audience trust relationship nên có key tách biệt. Nếu orders-api và billing-api dùng cùng secret, service nào cũng có thể mint token cho service còn lại.
Ký và verify JWT bằng Node.js jose
Các thời lượng chỉ là ví dụ. Chọn TTL theo threat model và UX. Kiểm tra API/version của dependency đang dùng và pin/update dependency theo security policy.
Middleware: Authentication trước, Authorization sau
JWT hợp lệ không có nghĩa request được phép truy cập mọi resource.
Không chỉ tin role: admin lâu dài trong token. Quyền có thể thay đổi trước khi token hết hạn. Dùng short TTL, version claim, policy lookup hoặc server-side authorization tùy độ nhạy.
Access Token và Refresh Token
Access token
Gửi tới resource server.
TTL ngắn.
Chứa claims tối thiểu.
Là bearer credential: ai giữ token có thể dùng token.
Refresh token
Chỉ gửi tới authorization server.
Sống lâu hơn.
Dùng để lấy access token mới.
Nên rotation theo mỗi lần sử dụng và phát hiện reuse.
Thường cần lưu hash, token family, trạng thái revoke và metadata ở server.
Mermaid
Loading diagram…
Đây là lý do JWT architecture thực tế thường không hoàn toàn stateless.
Logout và Revocation
JWT access token tự hợp lệ đến exp nếu chỉ verify bằng key. Các lựa chọn:
Short-lived access token: giảm cửa sổ rủi ro; logout xóa token ở client nhưng token bị đánh cắp còn hiệu lực tới expiry.
Denylist theo jti: lookup server-side; lưu TTL bằng thời gian còn lại của token.
User/session version: token chứa version; API hoặc cache kiểm tra version hiện tại.
Rotate/revoke signing key: khẩn cấp nhưng ảnh hưởng mọi token dùng key đó.
Opaque session/token: thuận lợi khi cần revoke tức thì và central control.
Không có lựa chọn miễn phí: stateless verification đổi lấy độ khó của immediate revocation.
Key Rotation với kid
Giữ key ring có trạng thái:
Quy trình:
Tạo key random mới.
Phân phối an toàn tới issuer/verifier.
Chuyển active kid.
Verify cả key mới và key cũ trong overlap window.
Đợi token cũ hết hạn.
Xóa key cũ và audit.
kid là input từ attacker. Chỉ map tới key trong allowlisted in-memory/configured key set. Không nối kid vào file path, SQL, URL hoặc lệnh shell; tránh path traversal, injection và SSRF.
Lưu JWT trên Browser
Không có vị trí hoàn hảo; chọn theo kiến trúc.
HttpOnly Secure Cookie
Ưu điểm:
JavaScript không đọc trực tiếp token, giảm token exfiltration khi XSS.
Trình duyệt tự gửi cookie.
Cần:
Secure.
HttpOnly.
SameSite=Lax hoặc Strict khi flow cho phép.
Phạm vi Domain/Path hẹp.
CSRF defense cho state-changing request khi cookie tự gửi.
XSS vẫn có thể thực hiện request dưới danh nghĩa user, dù không đọc được HttpOnly cookie.
Memory + BFF
Giữ token phía backend-for-frontend và dùng session cookie với browser thường giảm exposure cho SPA. Đây là kiến trúc có server state nhưng đôi khi an toàn và dễ revoke hơn.
localStorage
JavaScript và XSS có thể đọc token. Tránh lưu long-lived bearer token ở đó nếu có lựa chọn tốt hơn. CSP và chống XSS rất quan trọng nhưng không biến localStorage thành secret vault.
JWT và Session ID: chọn cái nào?
JWT không mặc định “tốt hơn session”. Với monolith cần logout/revoke tức thì, opaque session có thể đơn giản và an toàn hơn.
Anti-patterns thường gặp
Chỉ decode token mà không verify.
Tin alg từ header thay vì server allowlist.
Accept none hoặc trộn HS/RS không kiểm soát.
HMAC secret ngắn, có nghĩa hoặc dùng lại nhiều environment.
Đặt password/PII/secret trong payload.
Không kiểm tra iss, aud, exp, typ.
Access token sống nhiều ngày.
Dùng ID token gọi API như access token.
Dùng refresh token như access token.
Tin role/permission stale cho quyết định nhạy cảm.
Dùng cùng token profile cho password reset, email verification và API access.
kid điều khiển file path/URL/database query.
Log toàn bộ bearer token.
Nghĩ HTTPS thay thế signature validation.
Tự viết JWT parser hoặc HMAC.
Testing checklist
Token hợp lệ được chấp nhận.
Sửa header/payload/signature bị reject.
alg: none bị reject.
HS/RS confusion bị reject.
Sai iss, aud, typ bị reject.
Token expired hoặc chưa tới nbf bị reject.
Token vượt max lifetime bị reject.
Unknown/retired kid bị reject an toàn.
kid path traversal/SSRF payload không tạo I/O tùy ý.
Access token không dùng được ở refresh endpoint và ngược lại.
Token của service A không dùng được cho service B.
Refresh rotation và reuse detection hoạt động atomically.
Logout/revoke behavior đúng với tài liệu.
Authorization kiểm tra tenant/resource, không chỉ role claim.
Logs, metrics và traces không chứa raw token.
Production checklist
Dùng JOSE library được duy trì.
Algorithm allowlist cố định theo token profile.
Validate signature, issuer, audience, type và thời gian.
HMAC key random ít nhất 256 bit cho HS256.
Secret ở secret manager, tách environment/audience.
Access token TTL ngắn và claims tối thiểu.
Refresh token rotation, hash-at-rest và reuse detection.
Có chiến lược revoke/logout rõ ràng.
Rotation qua kid và overlap window có kiểm soát.
HTTPS mọi nơi; không đặt token trong URL.
Cookie có Secure, HttpOnly, SameSite và CSRF defense.
Rate limiting, audit và anomaly detection.
Authorization không chỉ dựa vào JWT hợp lệ.
Incident playbook cho key/token compromise.
Kết luận
JWT giúp truyền claims theo format compact và có thể được ký hoặc MAC. Với HS256, HMAC-SHA-256 bảo vệ integrity/authenticity bằng shared secret, nhưng không encrypt payload và không phải public-key digital signature.
JWT có thể loại bỏ session lookup khỏi hot path của API, nhưng không loại bỏ mọi server state. Refresh-token rotation, revoke, logout tức thì, account disable và permission freshness thường đưa state trở lại hệ thống.
Lựa chọn đúng không phải “JWT hay session” theo xu hướng, mà dựa trên trust boundary, revocation requirement, browser threat model và vận hành key. Nếu dùng JWT, hãy giữ access token ngắn hạn, validate toàn bộ token profile, bảo vệ key, tách authentication khỏi authorization và không tự viết crypto.
Vì sao raw SHA-256 không phù hợp để hash mật khẩu? So sánh MD5, SHA-256, Bcrypt, Argon2id và hướng dẫn password storage, migration, salt/pepper an toàn.