Explain how you would implement a custom authorization policy in ASP.NET Core Web API .
Question
Question: Explain how you would implement a custom authorization policy in ASP.NET Core Web API .
Brief Answer
Implementing a custom authorization policy in ASP.NET Core Web API provides a robust and flexible way to control access to your application’s resources, moving beyond simple role-based authorization to define complex rules based on user claims, external data, or even the resource being accessed.
The process typically involves four key steps:
- Define an Authorization Requirement: Create a simple class that implements
IAuthorizationRequirement. This class specifies what condition needs to be met for authorization (e.g.,MinimumAgeRequirement). It acts as a data container for the handler. - Implement an Authorization Handler: Create a class that inherits from
AuthorizationHandler<TRequirement>. This is where the actual authorization logic resides. You overrideHandleRequirementAsyncto evaluate the requirement against the user’s claims (viacontext.User) and other available context. Callcontext.Succeed(requirement)if the condition is met. - Register the Policy and Handler: In your application’s startup (e.g.,
Startup.csorProgram.cs), configure authorization options usingservices.AddAuthorization. Define your named policy usingoptions.AddPolicy("PolicyName", policy => policy.Requirements.Add(new YourRequirement()));. Crucially, you must also register your handler with the Dependency Injection container, typically as a singleton:services.AddSingleton<IAuthorizationHandler, YourHandler>();. - Apply the Policy: Enforce the policy by decorating your controllers or specific action methods with the
[Authorize]attribute, specifying your registered policy name using thePolicyproperty:[Authorize(Policy = "PolicyName")].
This approach offers significant advantages: it promotes decoupling of authorization logic from your business code, making it more maintainable and testable. It’s ideal for implementing fine-grained, dynamic rules (e.g., “a user can only edit their own post” – known as resource-based authorization where the resource is passed to the handler) that are difficult or impossible with simple roles. It heavily leverages claims-based authorization, allowing rules to be based on any piece of information associated with the user’s identity.
Super Brief Answer
Implementing a custom authorization policy in ASP.NET Core Web API enables flexible, fine-grained access control beyond simple roles, often based on user claims.
The process involves four core steps:
- Define a Requirement: A class (e.g.,
MinimumAgeRequirement) that specifies what condition must be met. - Implement a Handler: A class (e.g.,
MinimumAgeHandler) containing the logic to evaluate the requirement against user claims and context, callingcontext.Succeed()if successful. - Register Policy & Handler: Configure the named policy in
services.AddAuthorizationand register the handler with the DI container. - Apply Policy: Decorate controllers/actions with
[Authorize(Policy = "PolicyName")].
This method provides powerful, claims-based authorization, decoupling access logic for better maintainability and testability.
Detailed Answer
Implementing a custom authorization policy in ASP.NET Core Web API provides a robust and flexible way to control access to your application’s resources beyond simple role-based authorization. This approach allows you to define complex authorization rules based on various criteria such as user claims, external data, or even the resource being accessed.
Direct Summary
To implement a custom authorization policy, you typically follow a four-step process: first, define a custom authorization requirement that specifies the criteria for access; second, create an authorization handler that contains the logic to evaluate this requirement against the user’s context; third, register the policy and its handler within your application’s dependency injection container; and finally, apply the policy to your controllers or action methods using the [Authorize] attribute.
Key Concepts in Custom Authorization
Before diving into implementation, it’s helpful to understand the core components:
- Authorization: The process of determining whether an authenticated user has permission to perform an action or access a resource.
- Policy-based Authorization: A flexible authorization model in ASP.NET Core that allows you to define authorization rules as named policies, which are then applied to endpoints.
- Requirements: Simple classes that define a specific authorization condition that must be met. They act as data containers.
- Handlers: Classes that contain the logic to evaluate whether a user satisfies a given requirement.
- Claims-based Authorization: A fundamental part of ASP.NET Core authorization, where a user’s identity is represented by a set of claims (pieces of information about the user, e.g., name, email, roles, custom attributes). Policies often evaluate these claims.
Step-by-Step Implementation Guide
1. Define an Authorization Requirement
A requirement is a simple class that implements the IAuthorizationRequirement interface. It serves as a marker interface or can hold data necessary for the handler to perform its evaluation. It defines what needs to be authorized, not how.
Example: A MinimumAgeRequirement might hold an integer value for the required age.
// 1. Define a Requirement
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
2. Implement an Authorization Handler
The handler is where the actual authorization logic resides. It inherits from AuthorizationHandler<TRequirement>, where TRequirement is the specific requirement it handles. You must override the HandleRequirementAsync method.
Inside HandleRequirementAsync, you’ll access the user’s context (e.g., claims, roles) and other available resources to determine if the requirement is met. If the requirement is satisfied, you call context.Succeed(requirement). If it fails, you simply do not call context.Succeed; the authorization system will then fail the request by default if no handler succeeds for a given requirement.
// 2. Implement a Handler
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
// Assuming user has a "DateOfBirth" claim
var dateOfBirthClaim = context.User.FindFirst("DateOfBirth");
if (dateOfBirthClaim == null)
{
// User doesn't have the claim, requirement not met
return Task.CompletedTask;
}
if (DateTime.TryParse(dateOfBirthClaim.Value, out DateTime dateOfBirth))
{
var age = DateTime.Today.Year - dateOfBirth.Year;
// Adjust for birthday not yet passed this year
if (dateOfBirth.Date > DateTime.Today.AddYears(-age)) age--;
if (age >= requirement.MinimumAge)
{
// Requirement met: inform the authorization system
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
3. Register the Policy and Handler in Startup.cs
This step connects your requirement and handler to a named policy. In your Startup.cs file (or Program.cs in .NET 6+ minimal APIs), within the ConfigureServices method, you configure authorization options using services.AddAuthorization. You then add your policy using AddPolicy and associate it with your requirement. Crucially, you must also register your handler with the Dependency Injection (DI) container, typically as a singleton or transient service.
// 3. Register the Policy in Startup.cs -> ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
// ... other services
services.AddAuthorization(options =>
{
options.AddPolicy("MinimumAge21Policy", policy => // Renamed for clarity
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
// Register the handler with the DI container
// It's common to register all authorization handlers implementing IAuthorizationHandler
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
// ... other services
}
4. Apply the Policy to Controllers or Actions
Once the policy is registered, you can enforce it by decorating your controllers or specific action methods with the [Authorize] attribute, specifying the policy name using the Policy property.
// 4. Apply the Policy
[Authorize(Policy = "MinimumAge21Policy")] // Use the registered policy name
[ApiController]
[Route("[controller]")]
public class RestrictedController : ControllerBase
{
[HttpGet]
public IActionResult GetRestrictedData()
{
return Ok("You are old enough to access this data!");
}
}
Advanced Concepts and Interview Insights
When discussing custom authorization, interviewers often look for a deeper understanding of its practical applications and advantages. Be prepared to elaborate on the following:
Decoupling and Testability
Custom authorization policies promote a clean separation of concerns. By externalizing authorization logic into dedicated requirements and handlers, you prevent your controllers and business logic from becoming cluttered with access control code. This decoupling makes your codebase more maintainable and significantly improves testability. Each handler can be unit tested independently by mocking the AuthorizationHandlerContext, allowing you to verify specific authorization rules in isolation without needing to set up an entire application context.
Example: In a project requiring complex authorization based on user subscription levels and feature access, we used separate handlers for each rule (e.g., a SubscriptionLevelHandler and a FeatureAccessHandler). This modularity allowed us to unit test each rule meticulously and easily add or modify rules without impacting core application logic.
Differentiating Role-based vs. Policy-based Authorization
While role-based authorization is straightforward for simple access control (e.g., [Authorize(Roles = "Admin")]), it quickly becomes insufficient for more nuanced scenarios. Policy-based authorization offers significantly more expressiveness and fine-grained control.
- Role-based: Best for broad categories of users with predefined permissions. Simple to implement for basic access control.
- Policy-based: Essential for complex rules, such as “a user can only edit their own post” or “a user must have a certain claim value and be of a minimum age.” It allows for dynamic evaluation of conditions, often leveraging user claims.
Example: For a blog application, initial roles like “Author” and “Reader” sufficed. However, to implement “Authors can only edit their own posts,” we shifted to a policy like “MustBePostOwner,” with a handler evaluating the current user against the post’s author ID. This granular control is a key advantage of policies.
Resource-based Authorization
In scenarios where authorization depends on the specific resource being accessed (e.g., “Can this user delete *this specific* document?”), you can pass the resource object into the authorization context. This is achieved by passing the resource to the AuthorizationService.AuthorizeAsync method, which then makes the resource available in the AuthorizationHandlerContext.Resource property within your handler.
Example: For an e-commerce platform where users could only edit their own orders, the order ID was passed to the controller. Inside the action, the order object was retrieved and passed to AuthorizeAsync. Our custom handler then accessed the order from context.Resource to compare its owner ID with the current user’s ID, enabling precise, resource-specific authorization decisions.
Claims-based Authorization
Claims are fundamental to ASP.NET Core’s authorization model. A claim is a statement about a subject (user), such as their name, email, roles, or any custom attribute (e.g., “SubscriptionLevel: Premium”). When a user authenticates, their identity is represented by a ClaimsPrincipal containing these claims.
Custom authorization handlers heavily leverage claims. Inside the HandleRequirementAsync method, you access claims via context.User.Claims (or convenience methods like context.User.FindFirst("ClaimType")) to evaluate conditions. This makes policies incredibly flexible, as rules can be based on any piece of information associated with the user’s identity.
Example: A handler might check for a “Department” claim to restrict access to certain API endpoints to users only from the “HR” department, or a “PremiumUser” claim to unlock advanced features.

