.NET Deep Dive — Advanced Policy-Based Controller Action Security

Authorization is a crucial aspect of building secure web applications. In complex software systems with varying user roles and permissions, implementing authorization logic can quickly become tedious and unmaintainable. ASP.NET Core provides a powerful policy-based mechanism to elegantly handle authorization for MVC controllers and Razor pages. Let’s take a look !
Securing Pages and Controller
Let’s look at a basic example. Say we are building an e-commerce platform and need to restrict access to certain pages to logged in users only. We can easily do this by applying the [Authorize]
attribute to our Razor Page model:
[Authorize]
public class VipHousekeepingModel
: PageModel
{
}
The AuthorizeAttribute
will automatically redirect unauthenticated users to the login page. This saves us from having to manually check if a user is authenticated in page handler methods with an annoying if/else block.
We can also specify specific authorization requirements, like roles:
[Authorize(Roles = "Admin,PowerUser")]
public IActionResult AdminPortal() { }
But hardcoding roles everywhere has maintenance and separation of concerns issues. This is where policy-based authorization comes in. Don’t forget guys and girls, magic values
are against clean code.
Introducing Policy-Based Authorization
The policy-based authorization system allows us to centrally define authorization requirements in a startp class:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
{
policy.RequireRole("Admin");
});
});
}
We can now consume these policies via attributes:
[Authorize(Policy = "AdminOnly")]
public IActionResult AdminDashboard() { }
This keeps our controller code clean. Adding requirements like role checks, age restrictions etc. can be done in the central policy definitions.
Combining Multiple Requirements
Requirements can be combined using the RequireAssertion
policy builder method:
options.AddPolicy("AdminOrManager", policy =>
{
policy.RequireAssertion(context =>
context.User.IsInRole("Admin") ||
context.User.IsInRole("Manager"))
});
We can compose policies in this way to model complex authorization concerns elegantly.
Implementing Custom Requirements
Expand the capabilities of ASP.NET authorization by creating custom requirements. This is achieved through the IAuthorizationRequirement
interface.
Suppose we wish to restrict access to a page only for authenticated users with a mobile phone set. How do we proceed?
Drawing a parallel to the CQRS pattern, begin by creating a class derived from IAuthorizationRequirement
. Following this, create its corresponding handler derived from AuthorizationHandler<T>
, where T is our custom requirement class.
public sealed record LoggedVerifiedMemberRequirement
: IAuthorizationRequirement
{}
public class LoggedVerifiedMemberRequirementHandler
: AuthorizationHandler<LoggedVerifiedMemberRequirement>
{
private readonly ILogger<LoggedVerifiedMemberRequirementHandler> _logger;
public LoggedVerifiedMemberRequirementHandler(ILogger<LoggedVerifiedMemberRequirementHandler> logger)
{
_logger = logger;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
LoggedVerifiedMemberRequirement requirement)
{
_logger.LogInformation("Evaluating access for {UserName}", context.User.Identity.Name);
if (!IsUserAuthenticated(context))
{
_logger.LogInformation("User not authenticated");
return Task.CompletedTask;
}
if (!HasValidMobileClaim(context))
{
_logger.LogInformation("User has no mobile claim");
return Task.CompletedTask;
}
_logger.LogInformation("Access granted");
context.Succeed(requirement);
return Task.CompletedTask;
}
private bool IsUserAuthenticated(AuthorizationHandlerContext context)
{
return context.User.Identity.IsAuthenticated;
}
private bool HasValidMobileClaim(AuthorizationHandlerContext context)
{
return context.User.HasClaim(claim => claim.Type == ClaimTypes.MobilePhone);
}
}
Next, integrate the custom policy requirements into your Dependency Injection container.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("LoggedVerifiedMember", policy =>
{
policy.Requirements.Add(new LoggedVerifiedMemberRequirement());
});
});
Now, you can incorporate your newly crafted custom policy into your pages.
[Authorize(Policy = "LoggedVerifiedMember")]
public class HousekeepingModel
: PageModel
{
public void OnGet()
{
}
}
Access denied: A Closer Look
You do not have access to this resource.
By default, the ASP.NET Identity’s IdentityUser
will have a null value for the phone number. To grant access, you must set it in your profile via the application at /Identity/Account/Manage
.
However, there’s a catch! ASP.NET Identity doesn’t natively treat MobilePhone
as a user claim. You’ll need to manually add it. Here’s a sample implementation that facilitates this update:
Create a temporary page to handle the claim update:
public class UpdateClaim
: PageModel
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
public UpdateClaim(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
public async Task OnGetAsync()
{
var user = await _userManager.GetUserAsync(User);
var claim = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.MobilePhone);
if (claim != null && claim.Value != user.PhoneNumber)
{
await _userManager.RemoveClaimAsync(user, claim);
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.MobilePhone, user.PhoneNumber));
await _signInManager.RefreshSignInAsync(user);
}
else if (claim == null && !string.IsNullOrEmpty(user.PhoneNumber))
{
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.MobilePhone, user.PhoneNumber));
await _signInManager.RefreshSignInAsync(user);
}
}
}
After visiting this page, return to the Housekeeping page. Voila! You’re now a VIP.
Congratulation !
Conclusion
The policy-based approach makes complex authorization intuitive, scalable and maintainable. Centralizing requirements allows clean and expressive authorization.
Refer to the docs for advanced scenarios.