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. Obviously, that solution helps authorize users on the application level. I also promised 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. This 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. Indeed, we all heard about role-based authorization. It 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. Moreover, it 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. The application provides access to users with a specific IP Address. To demonstrate, I use a whitelist of IP Address. I will provide access only to the IP Addresses configured in the list. The list is in an array format in the appsettings.json. On the other hand, we can use a table-driven approach that allows the admin to add update entries easily.
- Create a new ASP.NET Core Web Application
- Select “Change Authentication” -> “Individual User Accounts”
- Run
Update-Database
from Package Manager Console - 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. The constructor 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 evaluates the requirements against a provided AuthorizationHandlerContext
. It inherits AuthorizationHandler<TRequirement>
and implements HandleRequirementAsync
method. Moreover, 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
Previously, 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 especially 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. Furthermore, we 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. Alternatively, the user must not 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
In short, 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.