Custom Handlers
Tự xây dựng Authentication & Authorization handler trong ASP.NET Core
Custom Authentication Handler
Khi các built-in schemes (Cookie, JWT) không đủ, bạn có thể tự tạo handler riêng. Ví dụ: xác thực qua API Key trong header.
Class Diagram
Implementation
// 1. Tạo Options class public class ApiKeyAuthOptions : AuthenticationSchemeOptions { public string HeaderName { get; set; } = "X-API-Key"; }
// 2. Tạo Handler public class ApiKeyAuthHandler : AuthenticationHandler<ApiKeyAuthOptions> { private readonly IApiKeyService _apiKeyService; public ApiKeyAuthHandler( IOptionsMonitor<ApiKeyAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, IApiKeyService apiKeyService) : base(options, logger, encoder) { _apiKeyService = apiKeyService; } protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { // Bước 1: Kiểm tra header có tồn tại không if (!Request.Headers.TryGetValue(Options.HeaderName, out var apiKey)) { return AuthenticateResult.NoResult(); // Không có key → bỏ qua scheme này } // Bước 2: Validate API key var keyInfo = await _apiKeyService.ValidateKeyAsync(apiKey!); if (keyInfo == null) { return AuthenticateResult.Fail("API key không hợp lệ"); // Key sai → fail } // Bước 3: Tạo ClaimsPrincipal var claims = new[] { new Claim(ClaimTypes.Name, keyInfo.Owner), new Claim(ClaimTypes.Role, keyInfo.Role), new Claim("api_key_id", keyInfo.Id.ToString()) }; var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return AuthenticateResult.Success(ticket); // Thành công! } }
// 3. Đăng ký trong Program.cs builder.Services.AddAuthentication("ApiKey") .AddScheme<ApiKeyAuthOptions, ApiKeyAuthHandler>("ApiKey", options => { options.HeaderName = "X-API-Key"; });
AuthenticateResult — 3 kết quả
Custom Authorization Handler
Tạo logic phân quyền tùy chỉnh cho các yêu cầu phức tạp. Pattern cơ bản: định nghĩa Requirement chứa dữ liệu, sau đó tạo Handler xử lý requirement đó.
Ví dụ: Business Hours Requirement
Chỉ cho phép truy cập trong giờ làm việc (8:00 AM – 6:00 PM):
public class BusinessHoursRequirement : IAuthorizationRequirement { public int StartHour { get; } = 8; // 8:00 AM public int EndHour { get; } = 18; // 6:00 PM }
public class BusinessHoursHandler : AuthorizationHandler<BusinessHoursRequirement> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, BusinessHoursRequirement requirement) { var currentHour = DateTime.Now.Hour; if (currentHour >= requirement.StartHour && currentHour < requirement.EndHour) { context.Succeed(requirement); } // Ngoài giờ làm việc → không Succeed // → requirement không thỏa mãn // Không gọi Fail() để handler khác // có thể override (VD: admin bypass) return Task.CompletedTask; } }
// Đăng ký builder.Services.AddAuthorization(options => { options.AddPolicy("BusinessHoursOnly", policy => policy.Requirements.Add(new BusinessHoursRequirement())); }); builder.Services.AddSingleton<IAuthorizationHandler, BusinessHoursHandler>();
context.Fail() trừ khi bạn muốn chắc
chắn từ chối,
bất kể các handler khác nói gì. Nếu chỉ đơn
giản là không gọi Succeed(),
handler khác vẫn có cơ hội thỏa mãn requirement (ví dụ:
admin bypass).
Dynamic Policy Provider
Khi bạn cần policies động — ví dụ tạo policy dựa trên
tên endpoint
hoặc dữ liệu từ database. Thay vì đăng ký từng policy
một, bạn
implement IAuthorizationPolicyProvider để tạo policy on-the-fly.
// Tạo policies động dựa trên tên // VD: [Authorize(Policy = "MinAge:18")] // → tự động tạo policy yêu cầu tuổi 18+ public class DynamicPolicyProvider : IAuthorizationPolicyProvider { private readonly DefaultAuthorizationPolicyProvider _fallback; public DynamicPolicyProvider( IOptions<AuthorizationOptions> options) { _fallback = new DefaultAuthorizationPolicyProvider(options); } public Task<AuthorizationPolicy?> GetPolicyAsync( string policyName) { // Parse policy name: "MinAge:18" if (policyName.StartsWith("MinAge:")) { var age = int.Parse( policyName.Substring("MinAge:".Length)); var policy = new AuthorizationPolicyBuilder() .AddRequirements(new MinimumAgeRequirement(age)) .Build(); return Task.FromResult<AuthorizationPolicy?>(policy); } // Fallback cho policy tĩnh return _fallback.GetPolicyAsync(policyName); } public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => _fallback.GetDefaultPolicyAsync(); public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => _fallback.GetFallbackPolicyAsync(); }
// Đăng ký builder.Services.AddSingleton<IAuthorizationPolicyProvider, DynamicPolicyProvider>();
DynamicPolicyProvider, bạn có thể tạo vô số policy
chỉ bằng naming convention.
Ví dụ: [Authorize(Policy = "MinAge:21")], [Authorize(Policy = "MinAge:13")]
—
không cần đăng ký từng cái một.
Patterns thực tế
Các pattern phổ biến khi xây dựng custom authentication và authorization trong production:
Client gửi API key qua header, server lookup trong database để xác thực:
- Đơn giản, không cần login flow
- Dễ revoke — chỉ cần xóa key khỏi database
- Phù hợp server-to-server communication
- Nên kết hợp với rate limiting để tránh abuse
Trong hệ thống multi-tenant, mỗi request cần được kiểm tra xem user có quyền truy cập tenant đang được yêu cầu hay không:
public class TenantRequirement : IAuthorizationRequirement { } public class TenantHandler : AuthorizationHandler<TenantRequirement> { private readonly IHttpContextAccessor _httpContext; public TenantHandler(IHttpContextAccessor httpContext) => _httpContext = httpContext; protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, TenantRequirement requirement) { var tenantId = _httpContext.HttpContext? .Request.Headers["X-Tenant-Id"] .FirstOrDefault(); var userTenants = context.User .FindAll("tenant_id") .Select(c => c.Value); if (tenantId != null && userTenants.Contains(tenantId)) { context.Succeed(requirement); } return Task.CompletedTask; } }
Thay vì dựa vào roles cứng, kiểm tra permissions từ database cho phép kiểm soát chi tiết hơn:
public class PermissionRequirement : IAuthorizationRequirement { public string Permission { get; } public PermissionRequirement(string permission) => Permission = permission; } public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { private readonly IPermissionService _permissions; public PermissionHandler(IPermissionService permissions) => _permissions = permissions; protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, PermissionRequirement requirement) { var userId = context.User .FindFirstValue(ClaimTypes.NameIdentifier); if (userId != null && await _permissions.HasPermissionAsync( userId, requirement.Permission)) { context.Succeed(requirement); } } } // Sử dụng: [Authorize(Policy = "Permission:posts.edit")]
Kết hợp authentication với rate limiting để giới hạn số request mỗi user. Có thể dùng authorization handler hoặc middleware:
public class UserRateLimitMiddleware { private readonly RequestDelegate _next; private readonly IDistributedCache _cache; public UserRateLimitMiddleware( RequestDelegate next, IDistributedCache cache) { _next = next; _cache = cache; } public async Task InvokeAsync(HttpContext context) { var userId = context.User .FindFirstValue(ClaimTypes.NameIdentifier); if (userId != null) { var key = $"rate:{userId}:{DateTime.UtcNow:HHmm}"; var count = await _cache.IncrementAsync(key); if (count > 100) // 100 requests/phút { context.Response.StatusCode = 429; return; } } await _next(context); } }
Testing Authentication & Authorization
Trong integration tests, bạn cần một cách để kiểm soát identity của user mà không cần thật sự đăng nhập. Giải pháp: tạo một test AuthenticationHandler.
// Test helper: fake authentication public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions> { public TestAuthHandler( IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) { } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { var claims = new[] { new Claim(ClaimTypes.Name, "TestUser"), new Claim(ClaimTypes.Role, "Admin") }; var identity = new ClaimsIdentity(claims, "Test"); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, "Test"); return Task.FromResult( AuthenticateResult.Success(ticket)); } }
// Trong integration test setup services.AddAuthentication("Test") .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>( "Test", _ => { });