Take advantage of Policy-based authorization, a simple, rich, expressive and reusable authorization model, to secure your applications written in ASP.NET Core
Authentication and Authorization are two terms you would often come across when reading about the security of web applications. While the former is used to validate a user’s credentials, the latter is used to grant access to one or more resources of the application to a user. There are two ways in which you can implement authorization in ASP.NET Core. These include role-based authorization and policy-based authorization. Role-based authorization has been in use from the previous versions of ASP.NET. Policy-based authorization has been newly introduced in ASP.NET Core and provides a rich, expressive and reusable authorization model to secure applications developed in ASP.NET Core. This article presents a discussion on how you can work with policy-based authorization in ASP.NET Core.
Prerequisites
To work with the code examples provided in this article, you should have Visual Studio 2017 and NET. Core installed in your system. If you don’t have .NET Core installed in your system, you can download a copy from here. You can download Visual Studio 2017 from this link.
Before the deep dive into how the policy-based authorization model works, here’s a quick tour of the role-based security model to understand the constraints of the role-based security model and then learn why the policy-based authorization model should be used.
Role-based Authorization in ASP.NET Core
A role is a string value that is mapped to a set of permissions for an authenticated user. The role-based security model has been in use from the days of ASP.NET. Role-based authorization is a declarative way to restrict access to resources. You can specify the roles that the current user must be a member of to access a specified resource. The Authorize
attribute enables you to restrict access to resources based on roles. It is a declarative attribute that can be applied to a controller or an action method. If you specify this attribute without any arguments, it only checks if the user is authenticated. Here’s an example that illustrates how this attribute can be applied to a controller.
1 2 3 4 5 6 7 |
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; [Authorize] public class UserController : Controller { //Action methods } |
The following code snippet can be used to limit access to the SecurityController
to only those users who are administrators, i.e., those users who are a member of the Administrator role.
1 2 3 4 5 |
[Authorize(Roles = "Administrator")] public class SecurityController : Controller { //Usual code } |
Multiple Roles
You can also specify multiple roles, separated by a comma. Here’s an example that illustrates this.
1 2 3 4 5 |
[Authorize(Roles = "Manager,Administrator")] public class DocumentsController : Controller { //Action methods } |
In this example, only those users who pertain to the Manager or Administrator role can have access to the DocumentsController
and all its methods.
Note that you can apply roles both at the controller and the action levels. As an example, even if users having either Manager or Administrator roles can access the DocumentsController
and its methods, you may want a method of this controller class to be accessed only by users who belong to the Administrator role. The following code snippet shows how this can be achieved.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[Authorize(Roles = "Manager, Administrator")] public class DocumentsController : Controller { public ActionResult ViewDocument() { //Your code here } [Authorize(Roles = "Administrator")] public ActionResult DeleteAllDocuments() { //Your code here } } |
Now, suppose you would want users who belong to both Manager and Administrator roles should only have access to the DocumentsController
and its methods. The following code snippet shows how you can define roles in such a way that the users who access the DocumentsController
and its methods must be a member of both Manager and Administrator roles.
1 2 3 4 5 |
[Authorize(Roles = "Manager")] [Authorize(Roles = "Administrator")] public class DocumentsController : Controller { } |
Albeit its ease of use, role-based authorization has its limitations. This is exactly why policy-based authorization is used. In the sections that follow you will examine how you can work with policy-based authorization in ASP.NET Core.
The Need for Policy-based Authorization
Authorization and access control using roles for protecting resources from unauthorized access have been in use for quite some time. However, they aren’t very expressive, and you may run into problems when the number of roles that are needed is significantly high.
Here’s a scenario with an example to explain. Suppose you are tasked with designing a security framework for an application. Initially, there are three roles in an application, i.e., User, Admin, and Manager. Now if you have multiple variations of the Admin role, like, CustomerAdmin, ReportsAdmin, and SuperAdmin, you would have to consider each of these when you are designing the security framework. You can also have multiple variations of the Manager role – your framework will have to consider these as well. As the number of these roles increases significantly, it becomes extremely difficult to effectively handle the roles. Here’s exactly where a policy-based authorization model comes in.
Working with Policy-based Authorization in ASP.NET Core
A policy-based security model decouples authorization and application logic and provides a flexible, reusable and extensible security model in ASP.NET Core. The policy-based security model is centered on three main concepts. These include policies, requirements, and handlers. A policy is comprised of several requirements. A requirement, in turn, contains data parameters to validate the user’s identity. Lastly, a handler is used to determine if a user has access to a specific resource. We’ll discuss each of these in more detail in this section – we’ll start with a policy.
Essentially, a policy is comprised of one or more requirements and is usually registered at application startup in the ConfigureServices()
method of the Startup.cs file. To apply the policies in your controllers or action methods, you can take advantage of the AuthorizeAttribute
attribute or the AuthorizeFilter
filter.
You can create a policy instance using the AuthorizationPolicyBuilder
class as shown in the code snippet given below. You can specify the role names using the RequireRole
method.
1 2 3 4 |
var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .RequireRole("Admin") .Build(); |
Alternatively, you can create the policy instance in the ConfigureServices
method as shown in the code snippet given below.
1 2 3 4 5 6 7 |
services.AddMvc(obj => { var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); obj.Filters.Add(new AuthorizeFilter(policy)); }); |
You’ll see more on this in the sections that follow.
Registering a Policy
Merely defining the policies isn’t enough – you should also register the policies you’ve defined with the authorization middleware. To register a policy, you should specify a name – this name would be used to reference the policy in the controller or the action methods.
The following code snippet illustrates how a policy can be registered at application startup in ASP.NET Core.
1 2 3 4 5 6 7 8 9 10 |
using Microsoft.Extensions.DependencyInjection; public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddAuthorization(options => { options.AddPolicy("RequireManagerOnly", policy => policy.RequireRole("Manager")); }); } |
You can also specify multiple allowed roles when registering the policy as shown in the code snippet below.
1 2 3 4 5 |
services.AddAuthorization(options => { options.AddPolicy("RequireManagerOnly", policy => policy.RequireRole("Manager","Administrator")); }); |
Applying the Policy
Once the policy has been registered, you can apply the policy in your controller or the controller’s action methods. If you were to apply the policy at the controller level, here’s how you would need to specify the policy.
1 2 3 4 5 |
[Authorize(Policy = "RequireAdminOnly")] public class SecurityController: Controller { //Action methods } |
As you can see, instead of specifying roles in the [Authorize]
attribute, you can specify the policy that you would like to apply. To apply a policy to an action method, you can take advantage of the Policy
property of the Authorize
attribute as shown in the code snippet below.
1 2 3 4 5 |
[Authorize(Policy = "RequireAdminOnly")] public IActionResult DeleteAllSecureDocuments() { //Some code } |
You can also apply multiple policies to a controller or an action method. The following code snippet illustrates how this can be achieved.
1 2 3 4 5 6 7 8 9 |
[Authorize(Policy = "ShouldBeEmployeeOnly")] public class SecurityController : Controller { [Authorize(Policy = "RequireAdminOnly")] public ActionResult DeleteUser() { //Your code here } } |
In the above code example, you can see two policies applied – one at the controller level and the other at the action level. Refer to the DeleteUser
action method in the code example above. This action method can be executed only by users who satisfy both the policies, namely ShouldBeEmployeeOnly and Administrator. In other words, to call the DeleteUser
method, the identity must fulfill the two policies ShouldBeEmployeeOnly and Administrator.
Although role-based authorization is easy to implement in ASP.NET Core, it has limited scope. As an example, imagine that you need to validate a user based on the Joining date or Department Id. You cannot have roles for each of such variations – that’s not a good solution at all. Here’s where you can take advantage of claims-based authorization – you can validate the identity of a user based on the claims. The section that follows examines how you can work with claims-based authorization via policies.
Using Claims Based Authorization via Policies
Claims based authorization provides a declarative way of checking access to resources. In this type of authorization, you would typically check the value of a claim and then grant access to a resource based on the value contained in the claim. First off, understand what a claim is. A claim is a key-value pair that represents a subject, i.e., name, age, passportnumber, drivinglicense, passport, nationality, dateofbirth, etc. So, if dateofbirth is the claim name, the claim value would be the date of birth, i.e., 1st January 1970.
It should be noted that a claim is given to a trusted party only. As mentioned above, a claim represents the subject – tells you who the subject is. However, a claim has nothing to do with what the subject can do – it never tells you that.
Claims based authorization can be implemented using policies. In this section, you will explore how you can work with policy-based claims. The code snippet given below shows how a simple claim policy is registered. The ShouldBeOnlyEmployee policy looks for the presence of the EmployeeId claim. The policy is added to the services collection using the AddPolicy
method.
1 2 3 4 5 6 7 8 9 |
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddAuthorization(options => { options.AddPolicy("ShouldBeOnlyEmployee", policy => policy.RequireClaim("EmployeeId")); }); } |
You can then apply this policy at the controller level on the AuthorizeAttribute
attribute as shown below.
1 2 3 4 5 |
[Authorize(Policy = "ShouldBeOnlyEmployee")] public IActionResult SomeMethod() { //Write your code here } |
You can have policies with multiple claims as well. You should register them appropriately in the ConfigureServices
method of the Startup
class as shown in the code snippet given below.
1 2 3 4 5 6 7 8 9 10 11 12 |
public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion( CompatibilityVersion.Version_2_1); services.AddAuthorization(options => { options.AddPolicy("CustomSecurityPolicy", policy => policy.RequireClaim("ShouldBeOnlyEmployee")); options.AddPolicy("CustomSecurityPolicy", policy => policy.RequireClaim("IsAdmin", "true")); }); } |
Requirements
Beneath the covers, both role-based and claims-based authorization techniques take advantage of a requirement, a handler, and a policy. In this and the subsequent sections each of these will be explored.
A requirement comprises a collection of data parameters. These data parameters are used by a policy to evaluate the user identity. To create a requirement, you need to create a class that implements the IAuthorizationRequirement
interface. The following code snippet illustrates a requirement for the MinimumExp policy – you will register this policy in a while.
1 2 3 4 5 6 7 8 |
public class MinimumExpRequirement : IAuthorizationRequirement { public int MinimumExp { get; set; } public MinimumExpRequirement(int experience) { MinimumExp = experience; } } |
Authorization Handlers
A requirement can have one or more handlers. An authorization handler is used to evaluate the properties of a requirement. To create an authorization handler, you should create a class that extends AuthorizationHandler<T>
and implements the HandleRequirementAsync()
method. The following code snippet shows how a typical authorization handler looks.
1 2 3 4 5 6 7 8 9 10 |
public class MinimumExpHandler : AuthorizationHandler<MinimumExpRequirement> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, MinimumExpRequirement requirement) { throw new NotImplementedException(); } } |
The following code snippet shows how you can write the necessary authorization logic in the HandleRequirementsAsync
method to find a claim and evaluate the requirement.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class MinimumExpHandler : AuthorizationHandler<MinimumExpRequirement> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, MinimumExpRequirement requirement) { var user = context.User; var claim = context.User.FindFirst("MinExperience"); if(claim != null) { var expInYears = int.Parse(claim?.Value); if (expInYears >= requirement.MinimumExp) context.Succeed(requirement); } return Task.CompletedTask; } } |
Now, how do I know whether a handler executed successfully, i.e., what should a handler return? Well, if a requirement has been successfully evaluated, you may want to call the Succeed
method on the AuthorizationHandlerContext
instance and pass the requirement instance as a parameter to the method. In the code snippet given above, note how the Succeed
method has been called.
Multiple Handlers for a Single Requirement
As I said earlier, a requirement can also have multiple handlers. You might want to use multiple handlers for a requirement when you need to evaluate the requirement based on multiple conditions. As an example, you might want to check if the user is an employee and if the age of the employee is more than 50 years. So, for each of these conditions, you need to have a separate handler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class EmployeeRequirement : IAuthorizationRequirement { //Write your code here } public class EmployeeRoleHandler : AuthorizationHandler<EmployeeRequirement> { //Write your code here to check if the user is an employee } public class MinimumAgeHandler : AuthorizationHandler<EmployeeRequirement> { //Write your code here to validate min age } |
Registering the handler
You should register handlers in the services collection. To register the handler created earlier in this article, you should write the following code in the ConfigureServices
method of the Startup
class as shown in the code snippet below.
1 2 3 4 5 6 |
public void ConfigureServices(IServiceCollection services) { //Other code services.AddSingleton<IAuthorizationHandler, MinimumExpHandler>(); } |
Note the usage of the AddSingleton
method in the ConfigureServices
method given above. When working with dependency injection in ASP.NET Core, you can specify the service lifetimes using AddTransient
, AddScoped,
or AddSingleton
methods. This example uses the singleton lifetime. When using this type of service lifetime, the service instance will be created the first time it is requested. Subsequent requests to the service will reuse the same instance. To know more on service lifetimes in ASP.NET Core, you can take a look at this article.
Here’s the complete code listing of the ConfigureServices
method – both the policy and the handler is registered with the pipeline.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion( CompatibilityVersion.Version_2_1); services.AddAuthorization(options => { options.AddPolicy( "MinExperience", policy => policy.Requirements.Add( new MinimumExpRequirement(5))); }); services.AddSingleton<IAuthorizationHandler, MinimumExpHandler>(); } |
Summary
The authorization model in ASP.NET Core has had a major upgrade with the introduction of a simple, declarative, policy-based authorization model. Policy-based authorization is flexible and helps you to build a loosely coupled security model by decoupling the authorization and application logic. Incidentally, ASP.NET Core supports both role-based and policy-based authorization. This article examined the policy-based authorization model, its benefits, and how to work with it in ASP.NET Core.
Load comments