JWT Best Practices: Access Token, Refresh Token và cơ chế thu hồi an toàn
JWT best practices thực chiến: access/refresh token, thời hạn, rotation, revoke/blacklist, session family, lưu trữ browser, validation, key rotation và checklist production.
ShanverseUpdated 12 min read— viewsLoading views
JWT
API Security
JWT không phải là session strategy hoàn chỉnh
JWT là định dạng biểu diễn claims có thể được ký hoặc mã hóa. Một access token dạng JWT cho phép resource server xác thực chữ ký và claims cục bộ mà không cần gọi authorization server cho từng request. Lợi ích này thường bị diễn giải quá mức thành “không cần lưu bất kỳ trạng thái nào”.
Một hệ thống production vẫn cần trạng thái cho:
Refresh-token sessions và rotation.
Logout, khóa tài khoản và đổi mật khẩu.
Thu hồi theo user, thiết bị, client hoặc authorization grant.
Phát hiện token replay và sự kiện rủi ro.
Key rotation, audit và incident response.
Nếu token còn hiệu lực 30 ngày và API chỉ kiểm tra chữ ký, việc “logout” ở frontend không làm token bị vô hiệu. Kẻ đã đánh cắp token vẫn dùng được cho đến exp
Comments
.
Access token và refresh token có vai trò khác nhau
Refresh token không nên được gửi tới resource API. Access token không nên dùng để xin access token mới. Tách audience, token type và validation rules để tránh một loại token bị dùng thay loại khác.
Tại sao access token phải sống ngắn?
Bearer token trao quyền cho bất kỳ ai đang cầm nó. Chữ ký chỉ chứng minh token do issuer hợp lệ phát hành và chưa bị sửa; chữ ký không chứng minh người gửi hiện tại là chủ sở hữu ban đầu.
Token sống càng lâu thì:
Cửa sổ khai thác sau khi token bị log, leak hoặc exfiltrate càng lớn.
Logout/khóa tài khoản có độ trễ lớn hơn nếu không có revocation lookup.
Claims như role, tenant, permission hoặc risk level dễ bị stale.
Key compromise tạo ra hậu quả dài hơn.
Incident response phải duy trì blacklist lớn và lâu hơn.
Nhiều hệ thống dùng access token khoảng 5–15 phút, nhưng đây là ví dụ, không phải con số chuẩn cho mọi ứng dụng. Chọn lifetime dựa trên giá trị dữ liệu, khả năng refresh, độ trễ revocation, môi trường client và yêu cầu UX. Giao dịch tài chính có thể cần token ngắn hơn và re-authentication; workload machine-to-machine có thể dùng chiến lược khác.
Claims tối thiểu cho access token
Một profile thường cần:
iss: issuer dự kiến.
sub: subject ổn định trong namespace của issuer.
aud: API/resource server cụ thể.
exp: thời điểm hết hạn.
iat: thời điểm phát hành.
nbf: nếu token chỉ có hiệu lực sau một thời điểm.
jti: định danh duy nhất khi cần revocation/replay tracking.
scope hoặc permissions tối thiểu.
typ: phân biệt access token với token khác khi profile hỗ trợ.
Không đưa password, secret, PII không cần thiết hoặc dữ liệu mà client không được đọc vào payload. Signed JWT không được mã hóa mặc định; người cầm token có thể đọc claims.
Validate đầy đủ, không chỉ verify signature
API phải kiểm tra một token profile cố định:
Sau đó kiểm tra exp, nbf, scope, subject, tenant và trạng thái authorization cần thiết. Thư viện thường xử lý time claims, nhưng phải cấu hình rõ.
Không:
Tin thuật toán từ header mà không có allowlist.
Chấp nhận alg: none.
Dùng chung validation rules giữa ID token, access token và refresh token JWT.
Chỉ decode() payload mà bỏ qua chữ ký.
Lấy key từ URL/header do token tùy ý chỉ định mà không kiểm soát issuer/JWKS.
Tin role trong token vô thời hạn sau khi quyền đã thay đổi.
Refresh token nên được thiết kế như thế nào?
Refresh token có thể là chuỗi opaque ngẫu nhiên thay vì JWT. Server chỉ cần lưu hash và metadata; không có yêu cầu phải tự chứa claims.
Một session record có thể gồm:
Chỉ lưu hash của refresh token giống cách lưu secret lookup. Nếu database bị đọc, raw token không nên dùng được trực tiếp. Sinh token bằng CSPRNG đủ mạnh và so sánh hash theo cách an toàn.
Refresh-token rotation
Mỗi lần refresh thành công:
Client gửi refresh token hiện tại.
Server hash token và tìm active session.
Server kiểm tra client binding, expiry, idle timeout và revoke state.
Server đánh dấu token cũ đã dùng/thay thế.
Server phát access token mới và refresh token mới.
Server giữ quan hệ trong cùng một token family.
Mermaid
Loading diagram…
Rotation không chỉ là thay token. Server phải giữ đủ trạng thái để nhận ra token cũ bị sử dụng lại.
Reuse detection và race condition
Nếu R1 đã đổi thành R2 nhưng R1 xuất hiện lần nữa, có hai khả năng:
Attacker đã đánh cắp R1.
Hai request hợp lệ refresh đồng thời do retry, nhiều tab hoặc network race.
Phản ứng an toàn thường là revoke cả token family, buộc đăng nhập lại và ghi security event. Tuy nhiên, cần xử lý concurrency có chủ đích:
Dùng transaction/compare-and-swap để chỉ một request thắng.
Có grace window rất ngắn nếu product cần retry tolerance.
Không để grace window biến token cũ thành reusable credential lâu dài.
Gắn idempotency hoặc coordination cho nhiều tab.
Quan sát device/session context trước khi quyết định mức phản ứng.
Absolute timeout và idle timeout
Rotation liên tục không được kéo dài session vô hạn. Áp dụng:
Access-token TTL: cửa sổ dùng trực tiếp tại API.
Refresh idle timeout: hết hạn nếu session không hoạt động trong N ngày.
Refresh absolute timeout: hết hạn bất kể hoạt động sau một mốc tối đa.
Step-up/re-auth timeout: yêu cầu xác thực lại cho hành động nhạy cảm.
Lifetime cụ thể phải lấy từ risk assessment, không copy máy móc từ tutorial.
Thu hồi token: có những mô hình nào?
1. Đợi access token hết hạn
Mô hình rẻ nhất: revoke refresh session và để access token ngắn tự hết hạn. Độ trễ revoke tối đa xấp xỉ access-token TTL. Phù hợp khi TTL ngắn và risk cho phép.
2. Denylist/blacklist theo jti
Lưu jti đã revoke đến khi token hết exp:
Mỗi API request kiểm tra chữ ký/claims rồi tra denylist:
Ưu điểm: revoke token cụ thể gần như ngay lập tức. Nhược điểm: thêm state, network lookup, cache consistency và availability dependency. TTL entry phải không vượt quá exp; không cần giữ blacklist vĩnh viễn.
Không nên lưu raw token làm key. Dùng jti ngẫu nhiên hoặc hash token để tránh lộ credential qua datastore/log.
3. Session/version check
Token chứa sid hoặc sessionVersion; API kiểm tra session active hoặc version hiện tại. Revoke một thiết bị bằng sid, revoke toàn user bằng tăng tokenVersion. Đây là state lookup nhưng thường dễ quản lý hơn blacklist từng token.
4. Opaque access token + introspection
Resource server gọi hoặc cache kết quả introspection từ authorization server. Thu hồi tập trung và linh hoạt hơn, đổi lại latency, coupling và yêu cầu availability/cache policy.
5. Event-driven revocation
Authorization server phát sự kiện logout/risk/revoke tới resource services, các service cập nhật local cache. Giảm lookup đồng bộ nhưng cần đảm bảo delivery, ordering, expiry và recovery.
Không có mô hình duy nhất tốt nhất. Hãy chọn theo SLA revocation, số lượng services, latency budget và consistency requirement.
Blacklist không làm JWT “vô nghĩa”
Một lập luận phổ biến là “đã tra Redis thì dùng JWT làm gì?”. JWT vẫn có thể cung cấp signed claims, giảm truy vấn user/profile ở hot path và cho phép nhiều resource server validate nhất quán. Nhưng khi cần revoke tức thì, một phần state là cái giá của security property đó.
Tối ưu theo nhu cầu:
Chỉ check denylist cho token có jti trong risk domain cần revoke.
Cache negative result ngắn hơn TTL token.
Dùng session version để giảm số entry.
Fail closed hay fail open khi revocation store lỗi phải là quyết định threat-model rõ ràng.
Đo latency và capacity; không âm thầm bỏ revocation check khi Redis chậm.
Logout đúng nghĩa
Logout nên:
Revoke refresh session/token family ở server.
Revoke access token hiện tại nếu yêu cầu logout tức thì; nếu không, chấp nhận cửa sổ TTL ngắn.
Xóa cookie/client state.
Ghi audit event.
Có tùy chọn logout một thiết bị hoặc tất cả thiết bị.
Với OIDC/federation, phân biệt local logout và IdP/session logout.
Chỉ xóa token ở frontend không phải server-side revocation.
Các sự kiện cần revoke
User logout.
Đổi/reset password.
Disable hoặc xóa tài khoản.
Thay đổi MFA hoặc recovery method.
Phát hiện refresh-token reuse.
Thiết bị mất/compromised.
Role/permission thay đổi có rủi ro cao.
Signing key hoặc client secret compromise.
Bất thường về địa lý, ASN, device hoặc hành vi.
Một số sự kiện cần revoke toàn user; số khác chỉ revoke một session/client.
Lưu token trong browser
Cho web app, ưu tiên BFF giữ OAuth tokens ở server và cấp session cookie opaque HttpOnly; Secure; SameSite cho browser. Nếu dùng cookie authentication, triển khai CSRF defense.
Nếu SPA bắt buộc giữ access token:
Ưu tiên memory và TTL ngắn.
Tránh refresh token dài hạn trong localStorage.
Dùng Authorization Code + PKCE.
Không nhúng client secret trong SPA.
Áp dụng CSP, XSS prevention và kiểm soát third-party scripts.
HttpOnly giảm khả năng JavaScript đọc token nhưng XSS vẫn có thể gọi API trong session hiện tại. Storage không thay thế lifecycle/revocation.
Key rotation
Mỗi key có kid duy nhất và được quản lý qua KMS/HSM khi phù hợp.
Verifier chỉ lấy JWKS từ issuer đã cấu hình.
Giữ public key cũ đủ lâu để verify token chưa hết hạn.
Không giữ key cũ lâu hơn policy cần thiết.
Có runbook revoke key khẩn cấp; sự cố private key có thể buộc invalidate nhiều token.
Dùng asymmetric signing khi nhiều services chỉ cần verify; tránh chia sẻ HMAC secret rộng.
Token lifetime dài làm key rotation và key compromise khó xử lý hơn vì verifier phải duy trì trust lâu hơn.
Đừng nhầm authentication và authorization
Một JWT hợp lệ không tự động cho phép action. Resource server vẫn phải kiểm tra:
Token đúng audience và token type.
Scope/permission phù hợp endpoint.
Subject được phép truy cập resource cụ thể.
Tenant boundary.
Trạng thái account/session nếu policy yêu cầu.
Step-up authentication cho thao tác nhạy cảm.
Không dùng ID token để gọi API trừ khi API profile định nghĩa rõ—thông thường access token mới là credential cho resource server.
Observability và incident response
Log metadata, không log raw token:
jti, sid, issuer, client ID, subject đã pseudonymize nếu cần.
Token family ID, rotation result, reuse detection.
Revoke reason và actor.
Resource/audience, scope, decision và latency.
Key ID và validation failure category.
Alert khi:
Refresh token cũ được reuse.
Một session refresh từ nhiều network/device bất thường.
Tỷ lệ invalid signature/audience tăng.
Refresh frequency vượt mức dự kiến.
Blacklist/introspection store lỗi hoặc latency cao.
Khi incident xảy ra, cần có khả năng revoke theo jti, sid, family, user, client, audience và key.
Checklist production
Access token có TTL ngắn dựa trên risk assessment.
Access token có audience/scope tối thiểu và jti khi cần revoke.
Refresh token dùng CSPRNG, lưu hash và chỉ gửi tới token endpoint.
Refresh token rotate sau mỗi lần dùng và có reuse detection.
Có idle timeout, absolute timeout và re-auth policy.
Logout revoke server-side, không chỉ xóa client storage.
Revocation SLA được định nghĩa và chọn denylist/session/introspection phù hợp.
Denylist entry hết hạn cùng token và không lưu raw token.
Validator allowlist algorithm và kiểm tra iss, aud, exp, nbf, typ.
ID/access/refresh tokens có validation profile tách biệt.
Browser storage và CSRF/XSS defenses phù hợp kiến trúc.
Signing keys có rotation và emergency runbook.
Có device/session management, audit và incident tooling.
Kết luận
JWT tốt không nằm ở việc token chứa bao nhiêu claims hay hệ thống “stateless” đến mức nào. Chất lượng nằm ở vòng đời credential.
Dùng access token ngắn để giới hạn cửa sổ thiệt hại. Dùng refresh token được rotate để duy trì UX mà không kéo dài quyền trực tiếp tại API. Giữ trạng thái session vừa đủ để phát hiện reuse và revoke. Dùng denylist khi cần thu hồi access token trước exp, nhưng đặt TTL và vận hành datastore đúng cách. Chính sự kết hợp này—không phải một token 30 ngày—mới tạo ra authentication có khả năng chống sự cố thực tế.
Hướng dẫn tích hợp cổng thanh toán VNPay, ZaloPay, MoMo và Stripe: payment flow, chữ ký webhook/IPN, idempotency, retry, reconciliation, refund và vận hành production.
Hướng dẫn kiến trúc Open Banking và Open API Spec: FAPI 2.0, OAuth 2.0/OIDC, PAR, PKCE, mTLS/DPoP, consent management, TPP onboarding, API security và vận hành.
Hướng dẫn tích hợp VietQR và QR Code động với webhook/IPN từ ngân hàng hoặc cổng thanh toán: xác minh chữ ký, chống replay, matching giao dịch, idempotency và reconciliation.