Auth trong .NET: JWT vs cookie, OIDC, refresh token
Cách chọn giữa JWT và cookie auth trong ASP.NET Core, khi nào outsource sang OIDC (Duende, OpenIddict, Keycloak), và refresh-token rotation chạy ra sao.
Mục lục
Authentication trông đơn giản - "đăng nhập user" - cho đến lúc scale. Sau đó nó đột nhiên là chuyện cờ bảo mật cookie, refresh-token rotation, validate JWT, OIDC discovery document, và intern chán đời phá được cái nào trên prod. Chương này cho cây quyết định cho .NET, kèm các failure mode quyết định mỗi nhánh.
Khi nào lựa chọn auth thực sự quan trọng?
Ba tín hiệu.
Hơn một loại client. App browser một mình lấy cookie miễn phí. Vừa thêm app mobile, app desktop, hay public API thì câu chuyện cookie hỏng - app mobile không dùng cookie cùng cách - và JWT cộng refresh trở nên hấp dẫn.
Hơn một service. Monolith validate session một chỗ. Đội microservice hoặc gọi auth service mỗi request (latency + coupling) hoặc nhận token tự chứa (JWT). Phương án thứ hai scale xa hơn.
Nhiều app chia sẻ identity. App khách + admin nội + portal partner, cùng kho user. Auth tự xây thành gánh bảo trì; OIDC sinh ra cho đúng ca này.
Nếu không cái nào đúng, cookie auth đơn giản nhất là lựa chọn đúng và giữ được nhiều năm.
Ngân sách số nào cho tier auth?
Chiến lược Chi phí validate Lưu trữ Thu hồi
Cookie + DB session ~1-3 ms (DB) server-side ngay
Cookie + Redis sess. ~0.5 ms (cache) Redis ngay
JWT (không DB) ~50 µs (chữ ký) stateless tới TTL
JWT + denylist Redis ~0.5 ms Redis ngay
OIDC (Duende) ~50 µs validate phía issuer token rotation
Lợi "không đụng DB mỗi request" của JWT có thật nhưng nhỏ ở quy mô hiện đại - 1 ms latency DB ở 10K QPS là 10 CPU-giây wall clock. Trade-off lớn hơn là thu hồi: JWT bị đánh cắp vẫn hợp lệ tới hạn trừ khi có denylist, đặt database trở lại đường nóng. Cookie session thu hồi ngay khi logout.
Kiến trúc auth tối thiểu trông thế nào?
Hai hình dạng.
Cookie auth (chỉ browser):
flowchart LR
Browser -->|POST /login| App[ASP.NET Core]
App --> DB[(User store)]
App -->|Set-Cookie<br/>HttpOnly,Secure| Browser
Browser -->|Cookie mỗi request| App
JWT + OIDC (đa client, đa service):
flowchart LR
Client[Web/Mobile/API] -->|đăng nhập| Auth[OIDC server<br/>Duende/OpenIddict/Keycloak]
Auth --> DB[(User store)]
Auth -->|access + refresh token| Client
Client -->|Authorization: Bearer| API1[ASP.NET Core API 1]
Client -->|Authorization: Bearer| API2[ASP.NET Core API 2]
Client -->|refresh| Auth
Chọn cái đơn giản hơn khi có thể. Hình OIDC thêm một service vận hành; chỉ lên cấp khi hình cookie không vừa với kiến trúc.
Cấu hình .NET 10 cho cookie auth?
Năm dòng cho đăng ký cộng endpoint login:
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(opt =>
{
opt.Cookie.Name = ".AnhTu.Auth";
opt.Cookie.HttpOnly = true;
opt.Cookie.SecurePolicy = CookieSecurePolicy.Always;
opt.Cookie.SameSite = SameSiteMode.Lax;
opt.ExpireTimeSpan = TimeSpan.FromDays(14);
opt.SlidingExpiration = true;
});
builder.Services.AddAuthorization();
builder.Services.AddAntiforgery();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
// Endpoint login:
app.MapPost("/login", async (LoginDto dto, IUserService users, HttpContext ctx) =>
{
var user = await users.VerifyAsync(dto.Email, dto.Password);
if (user is null) return Results.Unauthorized();
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email) };
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await ctx.SignInAsync(new ClaimsPrincipal(identity));
return Results.Ok();
});
Cookie HttpOnly nên JavaScript không đọc được (XSS an toàn);
Secure nên chỉ qua HTTPS; SameSite=Lax nên form xuyên site
không giả mạo (CSRF phần lớn được lo). Middleware anti-forgery
chèn các kẽ CSRF còn lại cho request thay đổi state.
Cấu hình .NET 10 cho JWT?
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.Authority = "https://auth.anhtu.dev";
opt.Audience = "api.anhtu.dev";
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromSeconds(30),
};
});
// Endpoint cần auth:
app.MapGet("/me", (ClaimsPrincipal user) =>
new { id = user.FindFirstValue(ClaimTypes.NameIdentifier) })
.RequireAuthorization();
URL Authority chỉ vào discovery document OIDC; ASP.NET Core fetch
key ký công khai, cache, và rotate tự động. API của bạn không bao
giờ nói với auth server trên đường nóng - validate JWT là việc CPU
local.
Tier auth tạo failure mode nào?
Năm cái phổ biến nhất:
- Đánh cắp token qua XSS - JavaScript đọc
localStorage, gửi token cho attacker. Phòng: không bao giờ lưu JWT tronglocalStorage; dùng cookie (HttpOnly) hoặc state in-memory. - Token replay - attacker đánh cắp refresh token. Phòng: refresh-token rotation; phát hiện reuse và thu hồi mọi session.
- Lệch đồng hồ - claim
expcủa JWT phụ thuộc đồng hồ đồng bộ. Lệch 1 phút có thể fail validate. Phòng: cho phépClockSkew = 30 s. - Lag rotate key - auth server xoay key ký; API cache key cũ. Phòng: TTL cache discovery document ngắn hơn, hoặc invalidate qua signal.
- Logout không logout - JWT hợp lệ tới hạn; user click "đăng xuất" mong thu hồi ngay. Phòng: access token ngắn (5-15 phút) cộng denylist cho refresh token đã thu hồi.
Chương 13 theo dõi
metric riêng auth: auth_failures_total,
token_validation_duration_seconds, refresh_reuse_alerts_total.
Khi nào nên bỏ qua OIDC hoàn toàn?
Khi bạn có một app, một team, không định share identity. Stack OIDC (Duende hay OpenIddict hay Keycloak) là một service phải chạy, một câu chuyện migration, một thứ vỡ lúc 3 giờ sáng. Cookie auth trên ASP.NET Core Identity là bề mặt nhỏ phủ mọi nhu cầu của bạn. Ước lượng QPS chương 2 và kiến trúc bạn chọn ở chương 5 cho bạn biết hình đơn giản có đủ không. Thêm OIDC vào ngày bạn thêm app thứ hai, không phải ngày bạn đọc chương này.
Đi tiếp đâu từ đây?
Bạn đã hoàn tất nhóm building blocks: cache, database, queue, API, search, auth. Chương kế tiếp: idempotency và pattern outbox
- pattern reliability đầu tiên gắn mọi thứ bạn học vào một hệ sống sót lỗi cục bộ.