Welcome to today’s post.
In today’s post, I will discuss how we can use a declarative authorization policy to secure your Web API applications.
In a previous post I showed how to apply resource-based (imperative) authorizations to secure your controllers and views.
An authorization policy is a rule or requirement that can be applied within an application to restrict access to a section or sections of code. The rule (or requirement) that defines the authorization policy can be dependent on data within a database that is run within the application, or on the currently logged in user.
There are two types of authorization policy that can be defined within a .NET Core application:
Declarative authorization
Imperative authorization
I will define these types of authorization policy in the next section.
Declarative vs Imperative Authorization
Declarative authorization allows authorization of resources to be checked before a controller action method is accessed. The implementation of the authorization rule can be built around policies and requirements, which when satisfied, allow access to the resource.
Imperative authorization allows authorization of resources to be embedded as code within the logic of controllers or views to allow more fine-grained access to the controller resource. With imperative authorisation we can use the IAuthorizationService and execute a custom authorization method.
Authorization policies consist of one or many requirements. The requirements can consist of CRUD requirements, or rule-based requirements, such as user age-based, or financial based restriction.
In this post, we will look at an example of how to use declarative policy-based authorization.
In the next section, I will show how to setup our .NET Core application to support the use of declarative authorization policies.
Setting up your Application for Authorization and Policy Definitions
In our startup source file, we will need to include the following namespace:
using Microsoft.AspNetCore.Authorization;
I will now show how to make changes to the startup sequence in out .NET Core application to allow authorization handlers to be applicable.
In your ConfigureServices() method in Startup.cs, setup authorization for your services collection as shown with the following code excerpt:
public void ConfigureServices(IServiceCollection services)
{
…
services.AddAuthorization(options =>
{
options.AddPolicy("BookReadAccess", policy =>
{
policy.AddRequirements(BookLoanOperations.Read);
});
options.AddPolicy("BookLoanPolicy", policy =>
{
policy.AddRequirements(new MinimumAgeRequirement(18));
});
});
…
}
The first policy BookReadAccess, adds a policy requirement parameter, BookLoanOperations.Read, which is a CRUD type security operation, defined in the following source (most unnecessary definitions are omitted):
BookOperations.cs:
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace BookLoan.Authorization
{
public static class BookLoanOperations
{
public static OperationAuthorizationRequirement Create =
new OperationAuthorizationRequirement {Name=”Create” };
public static OperationAuthorizationRequirement Read =
new OperationAuthorizationRequirement {Name=”Read” };
public static OperationAuthorizationRequirement Update =
new OperationAuthorizationRequirement {Name=”Update”};
public static OperationAuthorizationRequirement Delete =
new OperationAuthorizationRequirement {Name=”Delete”};
…
…
public static OperationAuthorizationRequirement Loan =
new OperationAuthorizationRequirement { Name = “Loan” };
}
}
The second policy BookLoanPolicy, adds a policy requirement parameter of type MinimumAgeRequirement, which is the class below that sets the threshold for a minimum age requirement:
MinimumAgeRequirement.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
namespace BookLoan.Authorization
{
public class MinimumAgeRequirement: IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
}
In your Configure() method you will then need to enable the authentication service for your application as follows:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
..
app.UseAuthentication();
..
}
In the next section, I will show how to implement the authorization handlers that can be used in our custom classes and application controllers.
Declarative Authorization Handlers based on Policies and Requirements
In this example we implement an authorization handler that can allow any user that is authenticated to view books.
namespace BookLoan.Authorization
{
public class BookReadAccessHandler: IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
var user = context.User;
var pendingRequirements = context.PendingRequirements.ToList();
foreach (var requirement in pendingRequirements)
{
if (requirement == BookLoanOperations.Read)
{
if (user.Identity.IsAuthenticated)
{
context.Succeed(requirement);
}
}
}
return Task.CompletedTask;
}
}
}
The PendingRequirement property is a list of the PendingRequirements of the calling context for the method. These are requirements that are not yet marked as being successful. In the above handler, if the list of pending requirements included in the Authorize attribute has a requirement matching BookLoanOperations.Read, and the user is authenticated, then the authorization will be successful.
Applying the Authorization Handler to Secure a Controller Method
In this section, will show how you can secure a controller action method by using the authorization policy handler using the familiar [Authorize] declaration with a Policy option.
In our Details(id) controller method, which is matched by the “Details/{id}” controller route, we decorate the header of the method with the Authorize attribute and the authorization policy BookReadAccess.
// GET: Book/Details/5
[Route("Details/{id}")]
[Authorize(Policy = "BookReadAccess")]
public async Task<ActionResult> Details(int id)
{
…
}
When the method is called, before the code block is entered, the authorization handler is executed with the policy BookReadAccess being added to the list of pending requirements. This is then matched against the operationBookLoanOperations.Read if the user is authenticated.
Applying Authorization Handlers to Secure a Controller Method based on User Role
In this section, I will show how to adapt the previous authorization handler, BookReadAccessHandler to alter the authorization rule so that authenticated users in a particular role can pass the authorization challenge.
In the next example of the authorization handler, we will allow a book to be created or edited only by an administrator user. We will assume that the user context includes custom roles.
Before we implement the authorization handler, we will need to setup the policy rules and requirements in the startup, like we did for the two previous authorization handlers.
The two authorization policies are setup with the Create and Update operations as shown:
using Microsoft.AspNetCore.Authorization;
…
// Authorization policies
services.AddAuthorization(options =>
{
options.AddPolicy("BookReadAccess", policy =>
{
policy.AddRequirements(BookLoanOperations.Read);
});
options.AddPolicy("BookUpdateAccess", policy =>
{
policy.AddRequirements(BookLoanOperations.Update);
});
options.AddPolicy("BookCreateAccess", policy =>
{
policy.AddRequirements(BookLoanOperations.Create);
});
options.AddPolicy("BookLoanPolicy", policy =>
{
policy.AddRequirements(new MinimumAgeRequirement(18));
});
});
If we were to setup some of the policies with single or multiple requirements then to satisfy the policy, we would need to have all the requirements within the policy rule to be satisfied before the challenge to the authorization policy in the handler can be successful.
The authorization handler BookUpdateAccessHandler to handle operations for the creation and update of book records is shown below:
public class BookUpdateAccessHandler : IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
var user = context.User;
var pendingRequirements = context.PendingRequirements.ToList();
foreach (var requirement in pendingRequirements)
{
if ((requirement == BookLoanOperations.Create) ||
(requirement == BookLoanOperations.Update))
{
if (user.IsInRole("Admin"))
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
}
You will also need to add the read and update authorization handlers to the service collection as shown so that we can inject them into classes within our application.
services.AddSingleton<IAuthorizationHandler, BookReadAccessHandler>();
services.AddSingleton<IAuthorizationHandler, BookUpdateAccessHandler>();
Applying the Authorize Attribute and Policy to Controller Methods
The authorization handlers we have implemented in the previous sections can now be used with controller methods to apply the requirements of the policies to restrict access to users within a specific role.
In the example below, access to the method Edit() has been restricted to users who are in the “Admin” role.
// GET: Book/Edit/5
[Route("Edit/{id}")]
[Authorize(Policy = "BookUpdateAccess")]
public async Task<ActionResult> Edit(int id)
{
…
}
When your application enters the route for \Edit or \Detail\{i}, the handlers above will be executed. If the following line of code:
context.Succeed(requirement);
is executed, then the controller action code block will be executed, else the application will redirect over to the action Account\AccessDenied.
That is all for today’s post.
I hope this has been a useful post on how to secure your controllers using policy-based authorizations.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.