Keywords: ASP.NET Core | Custom Authorization | Policy Authorization | IAuthorizationFilter | TypeFilterAttribute
Abstract: This article provides an in-depth exploration of various methods for implementing custom authorization attributes in ASP.NET Core, with a primary focus on policy-based authorization mechanisms and custom authorization filters. It details how to create dependency injection-enabled custom authorization attributes using TypeFilterAttribute combined with IAuthorizationFilter, and how to build flexible, extensible authorization systems through policies, requirements, and handlers. Through concrete code examples, the article demonstrates complete implementation processes ranging from simple authorization checks to complex business logic validation, offering practical technical guidance for developers.
Introduction
In modern web application development, authorization mechanisms are crucial components for ensuring system security. ASP.NET Core provides a robust authorization framework, but in practical projects, we often need to implement custom authorization logic based on specific business requirements. Unlike traditional ASP.NET, ASP.NET Core has undergone significant refactoring in its authorization mechanism, removing the AuthorizeCore method and introducing a more flexible and extensible authorization system.
Overview of ASP.NET Core Authorization Mechanism
The authorization system in ASP.NET Core is built upon identity authentication, defining access control rules in a declarative manner. The system provides multiple authorization approaches, including role-based authorization, claim-based authorization, and policy-based authorization. Among these, policy-based authorization is the officially recommended approach by Microsoft, which decomposes authorization logic into three core concepts: policies, requirements, and handlers, achieving high configurability and reusability of authorization logic.
Implementation Methods for Custom Authorization Attributes
Using TypeFilterAttribute and IAuthorizationFilter
In ASP.NET Core, while you cannot directly inherit from AuthorizeAttribute and override authorization methods, you can create custom authorization attributes by implementing the IAuthorizationFilter interface combined with TypeFilterAttribute. This approach allows access to services in the dependency injection container during the authorization process, providing support for complex authorization logic.
Here is an example implementation of a custom authorization attribute based on session ID validation:
public class ClaimRequirementAttribute : TypeFilterAttribute
{
public ClaimRequirementAttribute(string claimType, string claimValue) : base(typeof(ClaimRequirementFilter))
{
Arguments = new object[] { new Claim(claimType, claimValue) };
}
}
public class ClaimRequirementFilter : IAuthorizationFilter
{
readonly Claim _claim;
public ClaimRequirementFilter(Claim claim)
{
_claim = claim;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var hasClaim = context.HttpContext.User.Claims.Any(c => c.Type == _claim.Type && c.Value == _claim.Value);
if (!hasClaim)
{
context.Result = new ForbidResult();
}
}
}
When using this, we can apply the custom authorization attribute to controllers or action methods:
[Route("api/resource")]
public class MyController : Controller
{
[ClaimRequirement(MyClaimTypes.Permission, "CanReadResource")]
[HttpGet]
public IActionResult GetResource()
{
return Ok();
}
}
Policy-Based Authorization Implementation
Policy-based authorization is the recommended approach in ASP.NET Core, which decomposes authorization logic into three independent parts: policies, requirements, and handlers. This design makes authorization logic more modular and testable.
Here is a complete implementation of an age validation requirement:
public class Over18Requirement : AuthorizationHandler<Over18Requirement>, IAuthorizationRequirement
{
public override void Handle(AuthorizationHandlerContext context, Over18Requirement requirement)
{
if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
{
context.Fail();
return;
}
var dobVal = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value;
var dateOfBirth = Convert.ToDateTime(dobVal);
int age = DateTime.Today.Year - dateOfBirth.Year;
if (dateOfBirth > DateTime.Today.AddYears(-age))
{
age--;
}
if (age >= 18)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
}
Register the policy during application startup:
services.AddAuthorization(options =>
{
options.AddPolicy("Over18",
policy => policy.Requirements.Add(new Over18Requirement()));
});
Use the policy in controllers:
[Authorize(Policy = "Over18")]
public class RestrictedController : Controller
{
// Controller code
}
Dependency Injection in Custom Authorization
In practical applications, authorization logic often needs to access external services such as database connections, caching services, or other business logic services. By combining TypeFilterAttribute with dependency injection, we can inject required services into custom authorization filters.
Here is an example of a custom authorization filter that supports dependency injection:
public class SessionRequirementFilter : IAuthorizationFilter
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ISessionService _sessionService;
public SessionRequirementFilter(IHttpContextAccessor httpContextAccessor, ISessionService sessionService)
{
_httpContextAccessor = httpContextAccessor;
_sessionService = sessionService;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var sessionId = _httpContextAccessor.HttpContext!.Request.Headers["X-Session-Id"].FirstOrDefault();
if (string.IsNullOrEmpty(sessionId) || !_sessionService.ValidateSession(sessionId))
{
context.Result = new UnauthorizedResult();
return;
}
}
}
Performance Optimization Considerations
When implementing custom authorization logic, performance is a critical factor to consider. Here are some optimization recommendations:
- Avoid performing time-consuming database queries in authorization filters
- Use caching to store frequently accessed authorization data
- Reasonably design the granularity of policies and requirements to avoid performance overhead from excessive segmentation
- Consider using asynchronous authorization filters (
IAsyncAuthorizationFilter) for IO-intensive operations
Best Practices
Based on practical project experience, we summarize the following best practices:
- Prioritize policy-based authorization mechanisms as they provide better testability and maintainability
- Use custom authorization attributes for simple claim checks
- Ensure separation of authorization logic from business logic to maintain code clarity
- Write unit tests for custom authorization components to ensure logical correctness
- Consider security aspects and avoid exposing sensitive information during authorization processes
Conclusion
ASP.NET Core provides a flexible and powerful authorization framework that supports multiple custom authorization implementation methods. By appropriately selecting and using these mechanisms, developers can build authorization systems that are both secure and easy to maintain. Although policy-based authorization mechanisms have a steeper learning curve, they offer the best extensibility and testability, making them the preferred choice for large projects. For simple authorization requirements, custom authorization attributes provide a quick implementation path. Regardless of the chosen approach, understanding the working principles of authorization mechanisms and best practices is key to ensuring system security.