Policy based authorization in ASP.NET Core

Policy based authorization in ASP.NET Core

In my previous article, I explained about restricting users based on the IP Address.  It was implemented by using a whitelist of IP Address and middleware.  The solution helps authorize users on the application level.  I also mentioned that I will write another article to explain about restricting users on a controller level or action level.  Policy-based authorization is a new feature introduced in Dotnet Core that allows you to implement the application authorization rules in code.  In this post, I will explain about Policy-based authorization in ASP.NET Core with an implementation example.

Introduction

While authentication is to validate a user, authorization is to grant access to a resource of the application.  We all heard about role-based authorization, which provides access to the resources based on the role the user has.  Policy-based authorization, a new feature in the Dotnet core allows you to implement a loosely coupled security model.  This helps to decouple the authorization logic from controllers.

Key concepts

Policy-based authorization is based on the following key concepts.

First one is Requirement– Collection of data parameters used to evaluate the user

Second is Handler– Responsible for the evaluation of the requirement

Lastly Policy– Composed of one or more requirements

Let’s create these, one by one to implement Policy-based authorization.

Prerequisites

  • .NET Core 2.2 SDK
  • VS Code, Visual Studio 2017/2019
  • Familiarity with ASP.NET MVC

Getting started

I will explain the implementation of Policy-based authorization with a sample application that provides access to users with a specific IP Address. To demonstrate, I use a whitelist of IP Address and provide access only to the IP Addresses configured in the list.  The list is in an array format in the appsettings.json. Alternatively, we can use a table-driven approach that allows the admin to add update entries easily.

  1. Create a new ASP.NET Core Web Application
  2. Select “Change Authentication” -> “Individual User Accounts”
  3. Run “Update-Database” from Package Manager Console
  4. Finally, Run the application and test “Register” and “Login”

I have also seeded the Users and Roles table from Startup.cs.

Requirement

This provides vital data on the authorization requirement.  It implements the IAuthorizationRequirement interface.  In this case, I have a parameterized constructor that gets the ApplicationOptions object that contains the whitelist of IP Addresses.

public class IPRequirement : IAuthorizationRequirement
{
    public List<string> Whitelist { get; }
    public IPRequirement(ApplicationOptions applicationOptions)
    {
        Whitelist = applicationOptions.Whitelist;
    }
}

Handler

The next step is to create the handler.  It evaluated the requirements against a provided AuthorizationHandlerContext.  It inherits AuthorizationHandler<TRequirement> and implements HandleRequirementAsync method.  A requirement can have more than one handler.  Also, a handler may implement IAuthorizationHanlder to handle more than one requirement.

public class IPAddressHandler : AuthorizationHandler<IPRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IPRequirement requirement)
    {
        var authFilterCtx = (Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext)context.Resource;
        var httpContext = authFilterCtx.HttpContext;
        var ipAddress = httpContext.Connection.RemoteIpAddress;

        List<string> whiteListIPList = requirement.Whitelist;
        var isInwhiteListIPList = whiteListIPList
            .Where(a => IPAddress.Parse(a)
            .Equals(ipAddress))
            .Any();
        if (isInwhiteListIPList)
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}

Handlers should be registered in the service collection.  You have to do that in the ConfigureServices method of Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
          
    services.AddSingleton<IAuthorizationHandler, IPAddressHandler>();
    // Code omitted for brevity
}

Policy

We have created the requirement and handler.  Next, register the policy in the ConfigureServices method to apply that in Controllers.

public void ConfigureServices(IServiceCollection services)
{
    // Inject Application Options
    services.Configure<ApplicationOptions>(Configuration.GetSection("ApplicationOptions"));

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    var applicationOptions = Configuration
        .GetSection("ApplicationOptions")
        .Get<ApplicationOptions>();

    // Register Policy
    services.AddAuthorization(options =>
    {
        options.AddPolicy("RestrictIP", policy =>
            policy.Requirements.Add(new IPRequirement(applicationOptions)));

    });

    // Register Handler
    services.AddSingleton<IAuthorizationHandler, IPAddressHandler>();
}

Implementation

You can use the Authorize attribute to bind the policy to a Controller or specifically to an Action.

[Authorize(Policy = "RestrictIP")]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    [Authorize(Policy = "AnotherPolicy")]
    public IActionResult Privacy()
    {
        return View();
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}

Complex scenarios

Now you are familiar with creating a requirement and handler.  We also registered the Policy and apply that to a controller and action.  Let’s talk about little complex scenarios.  For example, having more than one handler for a requirement or handle more than one requirement in a handler.

 

Multiple handlers for a requirement

We create more than one handler for a requirement when there are multiple conditions that need to be evaluated.  The user will be authorized when one of the conditions is passed. For example, we want to allow access to a user with an email address from “blogofpi” domain OR the user doesn’t belong to a role other than “Contractor”.

Requirement

public class InternalUserRequirement : IAuthorizationRequirement
{
        
}

Handlers

To illustrate multiple handler requirements, I have created two handlers here and both inherit AuthorizationHandler<InternalUserRequirement>.  The policy evaluation is considered successful when one of the handlers succeeds.

public class EmailDomainHandler : AuthorizationHandler<InternalUserRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, InternalUserRequirement requirement)
    {
        var email = context.User.Identity.Name;
        var domain = email.Split('@')[1];
        if (domain.Equals("blogofpi.com"))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}
public class ExcludeContractorHandler : AuthorizationHandler<InternalUserRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, InternalUserRequirement requirement)
    {
        var roles = ((ClaimsIdentity)context.User.Identity).Claims
            .Where(c => c.Type == ClaimTypes.Role)
            .Select(c => c.Value);
        if (!roles.Contains("Contractor"))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}

Multiple requirements with a handler

Similarly, you can handle multiple requirements in a handler.  Please refer the following link for more details

Use handler for multiple requirements

Summary

This article tried to provide a summary of the Policy-based authorization.  To define your own authorization logic, you have to create an IAuthorizationRequirement requirement class.  Then create its associated AuthorizationHandler<TAuthorizationRequirement> implementation.  Lastly, add the requirement to a policy requirements collection in Startup.cs.

The full implementation of this post can be found in Github

 

Further Reading

The following are some of the links you can refer to if you want to learn more about Policy-based authorization in ASP.NET Core.

Leave a Reply

Close Menu
Share via
Copy link
Powered by Social Snap