While Role-Based Authorization handles broad access levels like Admin or User, real-world applications often require finer-grained control. What if access depends on a combination of claims, user age, geographic location, or whether the user owns the specific resource they are trying to edit?

For these complex scenarios, ASP.NET Core provides Policy-Based Authorization. A policy consists of one or more requirements and/or built-in assertions evaluated against the current user's claims.

This comprehensive guide covers everything from simple claim-based policies to building custom requirements and handlers from scratch.

Step 1: Configuring Policies in Program.cs

Policies are registered inside the dependency injection container when configuring authorization services in Program.cs. You can define policies using declarative helpers (like RequireClaim) or custom assertions.

Add your custom policies right before var app = builder.Build();:

C#

using Microsoft.AspNetCore.Authorization;

var builder = WebApplication.CreateBuilder(args);

// Add standard Identity & MVC services...
builder.Services.AddControllersWithViews();

// Register Policy-Based Authorization
builder.Services.AddAuthorization(options =>
{
    // 1. Department Claim Policy (Must be in Engineering or Support)
    options.AddPolicy("EmployeeOnly", policy =>
        policy.RequireClaim("Department", "Engineering", "Support"));

    // 2. Custom Assertion Policy (Must be at least 21 years old based on a claim)
    options.AddPolicy("AtLeast21", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c => c.Type == "DateOfBirth") &&
            DateTime.TryParse(context.User.FindFirst(c => c.Type == "DateOfBirth")?.Value, out var dob) &&
            dob.AddYears(21) <= DateTime.Today));
});

var app = builder.Build();

// Pipeline configuration...
app.UseAuthentication();
app.UseAuthorization();

Step 2: Creating Custom Requirements and Handlers

When your authorization logic requires complex data evaluation or database lookups, built-in assertions aren't enough. You need a Custom Requirement and an Authorization Handler.

1. Define the Requirement

Create a class implementing IAuthorizationRequirement:

C#

using Microsoft.AspNetCore.Authorization;

namespace MvcAuthenticationDemo.Authorization
{
    public class MinimumAgeRequirement : IAuthorizationRequirement
    {
        public int MinimumAge { get; }

        public MinimumAgeRequirement(int minimumAge)
        {
            MinimumAge = minimumAge;
        }
    }
}

2. Implement the Handler

Create a handler inheriting from AuthorizationHandler<TRequirement> that executes your evaluation logic:

C#

using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;

namespace MvcAuthenticationDemo.Authorization
{
    public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
    {
        protected override Task HandleRequirementAsync(
            AuthorizationHandlerContext context, 
            MinimumAgeRequirement requirement)
        {
            var dateOfBirthClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);
            if (dateOfBirthClaim == null)
            {
                return Task.CompletedTask; // Fail authorization implicitly
            }

            if (DateTime.TryParse(dateOfBirthClaim.Value, out var dateOfBirth))
            {
                var calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
                if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge)) 
                {
                    calculatedAge--;
                }

                if (calculatedAge >= requirement.MinimumAge)
                {
                    context.Succeed(requirement); // Mark authorization as successful
                }
            }

            return Task.CompletedTask;
        }
    }
}

3. Register the Handler in DI

Register your custom handler alongside your policies in Program.cs:

C#

using MvcAuthenticationDemo.Authorization;
using Microsoft.AspNetCore.Authorization;

// Register custom handler
builder.Services.AddScoped<IAuthorizationHandler, MinimumAgeHandler>();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Over21Only", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
});

Step 3: Applying Policies to Controllers and Action Methods

Once configured, applying a policy to any controller or individual action method is as simple as passing the policy name into the [Authorize] attribute:

C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

public class LoungeController : Controller
{
    // Restrict access using the custom age requirement policy
    [Authorize(Policy = "Over21Only")]
    public IActionResult BarAccess()
    {
        return View();
    }

    // Restrict access using the department claim policy
    [Authorize(Policy = "EmployeeOnly")]
    public IActionResult InternalDashboard()
    {
        return View();
    }
}

Step 4: Evaluating Policies Imperatively in Razor Views

Sometimes you don't want to block an entire page request with a 403 error, but rather show or hide specific UI components (like an "Edit" or "Delete" button) based on policy evaluation.

Inject IAuthorizationService directly into your Razor view:

HTML

@inject Microsoft.AspNetCore.Authorization.IAuthorizationService AuthorizationService

<div class="container mt-4">
    <h2>Dashboard</h2>

    @{
        // Imperatively evaluate a policy inside the view
        var authorizationResult = await AuthorizationService.AuthorizeAsync(User, "Over21Only");
    }

    @if (authorizationResult.Succeeded)
    {
        <div class="alert alert-success">
            <p>Exclusive Content: You are verified to view restricted materials.</p>
        </div>
    }
    else
    {
        <div class="alert alert-warning">
            <p>You must meet the age requirement to view this section.</p>
        </div>
    }
</div>

Conclusion

Policy-Based Authorization shifts your application's security away from rigid role lists and toward dynamic, rule-driven evaluation. By combining claim assertions, custom requirements, and imperative Razor checks, you can secure complex, enterprise-grade MVC architectures with confidence.