In this blog we'll see how we can implement Custom Authorization in ASP.NET Core. In the previous blog we learnt about Role Based Authrorization and how we can implement it. There we need to assign a role to a particular user to be able to access an endpoint. Here is the link to the previous blog Role Based Authorization in ASP.NET Core.
In the previous implementation it will only allow an endpoint to access if a required role is assigned. But, what if we need to check custom logic to be able to access an endpoint. In this blog, we'll see how we can implement it using Custom Authorization in ASP.NET Core.
Introduction
Custom Authorization allows us to check complex rules while giving access to a resource, where only role or claim check is not enough. In role or claim based check we rely simply on string check if the user has the corresponding role value matching or not. But, in custom one evaluation is based on dynamic variables at run time.
For e.g. we want to allow an endpoint to specific age group. This cannot be restricted through role based because age is changing everyday. So we need to calculate age from DateofBirth claim and allow or restrict access based on the logic. In this scenario, we need to check using Custom Authoriztion.
Apply Custom Authorization in ASP.NET Core
We'll apply custom authrorization step by step and will also learn the use of every step.
Let's take an example of our previous post. There we seeded an admin user and also associate an admin role to the user. We've done this because we intially want a user to access the system and do the admin operation, so that other user can user the system as per te requirement. Thus, we seeded the Admin user and role and also associate the role to admin user.
Instead of seeding data to the database like above, we can also allow some user email as superadmin user to operate on the system using Custom Authorization when the project is initially ran.
Let's apply Custom Authorization.
Define a Requirement
We've to create a attribute that implements IAuthorizationRequirementData. This attribute class is used to define the requirement i.e. what data need to validate against the logic during authorization check. This Attribute class contains only data that needs to be validated. This attribute will be associated with a controller or action mentod. If there is no data to be passed in the requirement, then this will be an empty class.
In our case email id is our requirement. This will be validated against the logged in user email whether it s admin email or not. So we'll define the requirement like below
using Microsoft.AspNetCore.Authorization;
namespace EmployeeManagement.Authorization
{
public class AllowedEmailAuthorizeAttribute(string email): IAuthorizationRequirement
{
public string AllowedEmail { get; set; } = email;
}
}
The admin user email will be passed with the attribute like this [AllowedEmailAuthorize("admin@mail.com")] from the controller or action method, where we want to add the custom authorization.
Define a handler to handle the requirement
The handler is a class contains the actual execution logic. It take the requirement and execute the authorization logic based on the requirement. The handler class inherits from AuthorizationHandler<TRequirement> and overrides the HandleRequirementAsync method. Under this method the logic is placed. If the condition is satisfied it calls the context.Succeed(requirement). If not then context will fail silently.
Here is the AllowedEmailAuthorizationHandler.cs
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
namespace EmployeeManagement.Authorization
{
public class AllowedEmailAuthorizationHandler : AuthorizationHandler<AllowedEmailAuthorizeAttribute>
{
//Overrides the HandleRequirementAsync from AuthorizationHandler<TRequirement>
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AllowedEmailAuthorizeAttribute requirement)
{
//Get the user email claim
var userEmailClaim = context.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Email);
if (userEmailClaim != null)
{
var userEmail = userEmailClaim?.Value?.ToString();
//If the email matches with the requirement, succeed the authorization requirement
if (string.Equals(userEmail, requirement.AllowedEmail, StringComparison.OrdinalIgnoreCase))
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
}
Register the handler service
We need to register the above class as a singleton IAuthorizationHandler service in program.cs file.
builder.Services.AddSingleton<IAuthorizationHandler, AllowedEmailAuthorizationHandler>();
Add the requirement attribute to Controller/Action methods
We've defined our requirement also the handler for the requirement. Now, we've to add the attribute to the controller/action methods where we want to restrict the resouces using custom Authorization.
In our case we want the RoleController will be restricted using custom Authorization. Let's apply.
using EmployeeManagement.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace EmployeeManagement.Controllers
{
[Route("api/[controller]")]
[ApiController]
[AllowedEmailAuthorize("admin@mail.com")]
public class RolesController(RoleManager<IdentityRole> roleManager) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAll()
{
return Ok(await roleManager.Roles.ToListAsync());
}
}
}
Test Custom Authorization
Let's start the application. We'll create two user to test this implementation. One with the email mentioned in the AllowedEmailAuthorizeAttribute and second with another email.
Let's try with another email first.
Now, lets try with the email provided in AllowedEmail Authorize attribute.
Conclusion
In this blog we learnt what is custom authorization and how to implement it in ASP.NET Core. This type of authorization is helpful when we have some business logic to execute during the authorization check, where normal Role or Claim matching is not enough. We can reuse the requirement on multiple places with different data. It also provides centralized logic for each handler. Instead of calling the attribute directly, we can register a policy and use it, which we'll see in upcoming blogs.

Comments
Post a Comment