Chapter 05

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

AuthenticationHandler<TOptions> ← Abstract base class
#HandleAuthenticateAsync()← Override method này
#HandleChallengeAsync()← Xử lý 401
#HandleForbiddenAsync()← Xử lý 403
kế thừa
ApiKeyAuthHandler ← Handler tùy chỉnh
+HandleAuthenticateAsync()
→ Đọc header "X-API-Key"
→ Validate key trong database
→ Trả về AuthenticateResult

Implementation

C# — Options
// 1. Tạo Options class
public class ApiKeyAuthOptions : AuthenticationSchemeOptions
{
    public string HeaderName { get; set; } = "X-API-Key";
}
C# — Handler
// 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!
    }
}
C# — Registration
// 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ả

Success(ticket)
User authenticated, ClaimsPrincipal được set vào HttpContext
⏭️
NoResult()
Bỏ qua scheme này, thử scheme khác nếu có
Fail(message)
Authentication thất bại, trả 401 Unauthorized

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 đó.

interface IAuthorizationRequirement
implements
chứa data CustomRequirement
xử lý bởi
handler AuthorizationHandler<CustomRequirement>
gọi
kết quả context.Succeed() / context.Fail()

Ví dụ: Business Hours Requirement

Chỉ cho phép truy cập trong giờ làm việc (8:00 AM – 6:00 PM):

C# — Requirement
public class BusinessHoursRequirement : IAuthorizationRequirement
{
    public int StartHour { get; } = 8;   // 8:00 AM
    public int EndHour { get; } = 18;    // 6:00 PM
}
C# — Handler
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;
    }
}
C# — Registration
// Đăng ký
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("BusinessHoursOnly", policy =>
        policy.Requirements.Add(new BusinessHoursRequirement()));
});
builder.Services.AddSingleton<IAuthorizationHandler, BusinessHoursHandler>();
Không gọi 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.

C# — DynamicPolicyProvider
// 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();
}
C# — Registration
// Đăng ký
builder.Services.AddSingleton<IAuthorizationPolicyProvider,
    DynamicPolicyProvider>();
Với 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:

01
API Key Authentication
Phổ biến cho internal APIs và service-to-service communication

Client gửi API key qua header, server lookup trong database để xác thực:

Client gửi header X-API-Key Server lookup trong DB Match → Tạo identity
  • Đơ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
02
Multi-Tenant Authorization
Kiểm tra user có thuộc tenant hiện tại không

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:

C#
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;
    }
}
03
Permission-Based Authorization
Kiểm tra permissions chi tiết thay vì roles

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:

C#
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")]
04
Rate Limiting per User
Giới hạn số request mỗi user trong khoảng thời gian

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:

C# — Middleware approach
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.

C# — Test Handler
// 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));
    }
}
C# — Test Setup
// Trong integration test setup
services.AddAuthentication("Test")
    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
        "Test", _ => { });
Trong tests, tạo test AuthenticationHandler giúp bạn kiểm soát hoàn toàn identity của user mà không cần thật sự đăng nhập. Rất hữu ích cho integration testing — bạn có thể test các authorization policy với bất kỳ claims nào bạn muốn.