Implementing Policy-Based and Role-Based Authorization in .NET Core

This article is all about policy-based authorization using .NET Core. Please refer to the link if you need to learn the basics of the authorization techniques. Let’s break down the key components:

  1. Authentication Setup

    • First, set up authentication in your application. This ensures that users are properly identified and authenticated.
    • You’ll typically use a third-party authentication provider (like Microsoft Identity Platform) to handle user logins and issue tokens.
  2. Authorization Policies

    • Next, define authorization policies. These policies determine who can access specific parts of your application.
    • Policies can be simple (e.g., “authenticated users only”) or more complex (based on claims, roles, or custom requirements).
  3. Default Policy

    • Create a default policy that applies to all endpoints unless overridden.
    • For example, you might require users to be authenticated and have specific claims (like a preferred username).
  4. Custom Policies

    • Add custom policies for specific scenarios. These allow fine-grained control over access.
    • For instance, you can create policies based on permissions (e.g., “create/edit user” or “view users”).
  5. Permission Requirements

    • Define permission requirements (e.g., PermissionAuthorizationRequirement). These represent specific actions or features.
    • For each requirement, check if the user has the necessary permissions (based on their roles or other criteria).
  6. Role-Based Authorization

    • Optionally, incorporate role-based authorization.
    • Roles group users with similar access levels (e.g., “admin,” “user,” etc.). You can assign roles to users.
  7. Authorization Handlers

    • Implement custom authorization handlers (e.g., AuthorizationHandler).
    • These handlers evaluate whether a user meets the requirements (e.g., has the right permissions or roles).
  8. Controller Actions

    • In your controller actions, apply authorization rules.
    • Use [Authorize] attributes with either policies or roles.
  9. Middleware Result Handling

    • Customize how authorization results are handled (e.g., 401 Unauthorized or 403 Forbidden responses).
    • You can create an AuthorizationMiddlewareResultHandler to manage this behavior

Let's go ahead will the actual coding for your Web API:

Program.cs: User Azure AD authentication as it is. Decide your Permission Keys for Authorization.

Only one policy in the AddPolicy method

//In Program.cs file, add the below code 
     
            var builder = WebApplication.CreateBuilder(args);

            // Authentication using Microsoft Identity Platform
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

            // Default policy-based authorization
            services.AddAuthorizationCore(options =>
            {
                options.DefaultPolicy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .RequireClaim("preferred_username")
                    .RequireScope("user_impersonation")
                    .Build();

                options.AddPolicy("Permission1", policy =>
                    policy.Requirements.Add(new PermissionAuthorizationRequirement("Permission1")));

                options.AddPolicy("Permission2", policy =>
                    policy.Requirements.Add(new PermissionAuthorizationRequirement("Permission2")));
            });

            // Authorization handler
            services.AddScoped<IAuthorizationHandler, AuthorizationHandler>();

            // Middleware result handler for response errors (401 or 403)
            services.AddScoped<IAuthorizationMiddlewareResultHandler, AuthorizationMiddlewareResultHandler>();

            // Other services and configurations...


PermissionAuthorizationRequirement .cs: Just copy and paste the requirement

//Create new PermissionAuthorizationRequirement.cs file for Custom requirement for permission-based authorization
    public class PermissionAuthorizationRequirement : IAuthorizationRequirement
    {
        public PermissionAuthorizationRequirement(string allowedPermission)
        {
            AllowedPermission = allowedPermission;
        }

        public string AllowedPermission { get; }
    }

 AuthorizationHandler.cs: (Just copy and paste the code. Make sure to hit the DB call to get the permissions list by using App Manager)

// Custom authorization handler to check user permissions
    public class AuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
    {
        // Business layer service for user-related operations
        private readonly IAppManager _appManager;

        public AuthorizationHandler(IAppManager appManager)
        {
            _appManager= appManager;
        }

        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
        {
            // Find the preferred_username claim
            var preferredUsernameClaim = context.User.Claims.FirstOrDefault(c => c.Type == "preferred_username");

            if (preferredUsernameClaim is null)
            {
                // User is not authenticated
                context.Fail(new AuthorizationFailureReason(this, "UnAuthenticated"));
                return;
            }

            // Call the business layer method to check if the user exists
            var user = await _appManager.GetUserRolesAndPermissions(preferredUsernameClaim);

            if (user is null || !user.IsActive)
            {
                // User does not exist or is inactive
                context.Fail(new AuthorizationFailureReason(this, "UnAuthenticated"));
                return;
            }

            // Select the list of permissions that the user is assigned 
            // Here you will fetch the Permission1 and Permission2 
            var userPermissions = user.UserPermissions?.Select(k => k.PermissionKey);

            // Get the current permission key from the controller's action method
            string allowedPermission = requirement.AllowedPermission;


            // Check if the current request carries this permission 
            if (userPermissions.Any(permission => permission == allowedPermission))
            {
                // Permission granted
                context.Succeed(requirement);
                return;
            }

            // Permission denied
            context.Fail();
        }
    }

AuthorizationMiddlewareResultHandler.cs: Just copy and paste. No changes are required.

// AuthorizationMiddlewareResultHandler to decide response code (401 or success)
public class AuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
    private readonly ILogger<AuthorizationMiddlewareResultHandler> _logger;
    private readonly Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler _defaultHandler = new();

    public AuthorizationMiddlewareResultHandler(ILogger<AuthorizationMiddlewareResultHandler> logger)
    {
        _logger = logger;
    }

    public async Task HandleAsync(
        RequestDelegate next,
        HttpContext context,
        AuthorizationPolicy policy,
        PolicyAuthorizationResult authorizeResult)
    {
        var authorizationFailureReason = authorizeResult.AuthorizationFailure?.FailureReasons.FirstOrDefault();
        var message = authorizationFailureReason?.Message;

        if (string.Equals(message, "UnAuthenticated", StringComparison.CurrentCultureIgnoreCase))
        {
            // Set response status code to 401 (Unauthorized)
            context.Response.StatusCode = "401";
            _logger.LogInformation("401 failed authentication");
            return;
        }

        // If not unauthorized, continue with default handler
        await _defaultHandler.HandleAsync(next, context, policy, authorizeResult);
    }
}

Controller.cs: Finally, in the dashboard controller, add the attributes [Authorize] and [Policies]. Here you will define Permission1 and Permission2

// Dashboard controller
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class DashboardController : ControllerBase
{
    [HttpGet]
    [Route("get-dashboardDetails")]
    [Authorize(Policy = "Permission1")]
    public async Task GetAllDashboardDetailsAsync()
    {
        // Your logic for fetching all user details
        return await GetAllDashboardDetailsAsync();
    }

    [HttpGet]
    [Route("create-product")]
    [Authorize(Policy = "Permission2")]
    //[Authorize(Policy = nameof(Read string from ENUM))]
    public async Task CreateProductAsync([FromBody] Product product)
    {
        // Your logic for creating a new product
    }
}

Now if you want to combine with Roles based authorization in your Web API code. Follow the below steps or you can avoid moving forward. It is almost the same steps as we did with the policy-based authorization with a small difference that you will figure out in the code.

1. Register the RoleAuthorizationHandler: In your Program.cs file, just add the following line to register the RoleAuthorizationHandler

builder.Services.AddScoped<IAuthorizationHandler, RoleAuthorizationHandler>();

2. RoleAuthorizationHandler: Below is the RoleAuthorizationHandler.cs one that checks whether the user has the required roles. Create one more handler 

public class RoleAuthorizationHandler : AuthorizationHandler<RolesAuthorizationRequirement>
{
    private readonly IAppManager _appManager;

    public RoleAuthorizationHandler(IAppManager appManager)
    {
        _appManager= appManager;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement)
    {
        // Find the preferred_username claim
        Claim claim = context.User.Claims.FirstOrDefault(c => c.Type == "preferred_username");

        if (claim is not null)
        {
            // Get user details
            var userRoles = await _appManager.GetUserRolesAndPermissions(claim.Value);

            // Check if the user's roles match the allowed roles
            var roles = requirement.AllowedRoles;
            if (userRoles .Any(x => roles.Contains(x)))
            {
                context.Succeed(requirement); // User has the required roles
            }
            else
            {
                context.Fail(); // User does not have the required roles
                return;
            }
        }
        await Task.CompletedTask;
    }
}

3. Usage in DashboardController: In your DashboardController, you can use both roles and policies for authorization. For example:

    [HttpGet]
    [Route("get-dashboardDetails")]
    [Authorize(Roles = "Super_Admin, Role_Administrator")] // Can have multiple roles. You can choose either Roles or Policy or both
    [Authorize(Policy = "Permission1")] // Can have only one policy per action to accept.
    public async Task GetAllDashboardDetailsAsync()
    {
        // Your logic for fetching all user details
        return await GetAllDashboardDetailsAsync();
    }

That's all. Your Web API will work like magic. :-) Stay tuned for more learning.