Welcome to today’s post.
In today’s post, I will discuss how we can use imperative authorization policy to secure your Web API applications.
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 authorization 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 imperative policy-based authorization.
In the next section, I will show how to setup our .NET Core application to support the use of imperative 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 our .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("BookLoanAccess", policy =>
{
policy.AddRequirements(BookLoanOperations.Loan);
});
options.AddPolicy("BookLoanAgeRestriction", policy =>
{
policy.AddRequirements(new MinimumAgeRequirement(18));
});
});
…
}
In the above options for the authorization service, I have defined two policies:
- A policy for book loan access.
- A policy for minimum age requirements.
The first policy BookLoanAccess, adds a policy requirement parameter, BookLoanOperations.Loan, which is an 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 Loan =
new OperationAuthorizationRequirement { Name = “Loan” };
}
}
The second policy BookLoanAgeRestriction, addsa 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.
Implementation of Resource-based Authorization Handlers
The authorization handler can include different criteria with which we can challenge the user authorization of the resource.
In the handler BookLoanAccessHandler below, we include parameters that include the user context, a data resource, and a policy requirement.
BookLoanAccessHandler.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using BookLoan.Services;
namespace BookLoan.Authorization
{
public class BookLoanAccessHandler : AuthorizationHandler<BookLoanRequirement, ReportService>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
BookLoanRequirement requirement,
ReportService resource)
{
Task<bool> anyOverdueLoansResult = resource.CurrentUserAnyOverdueLoans();
bool anyOverdueLoans = anyOverdueLoansResult.GetAwaiter().GetResult();
if (!anyOverdueLoans)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
Below is the policy requirement used in the above authorization hander, which is declared without any parameters so that it can be used within a resource-only authorization handler:
namespace BookLoan.Authorization
{
public class BookLoanRequirement: IAuthorizationRequirement
{
public BookLoanRequirement()
{
}
}
}
A sample ReportService resource is a data service that stores or returns additional attributes or results for our handler to interpret based on data dependent on the user.
ReportService.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BookLoan.Data;
using BookLoan.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using BookLoan.Models;
namespace BookLoan.Services
{
public class ReportService: IReportService
{
ApplicationDbContext _db;
ILogger _logger;
IBookService _bookService;
HttpContext _context;
public ReportService(ApplicationDbContext db,
UserManager<ApplicationUser> userManager,
IHttpContextAccessor httpContextAccessor,
IBookService bookService,
ILogger<ReportService> logger)
{
_db = db;
_logger = logger;
_bookService = bookService;
_context = httpContextAccessor.HttpContext;
}
public async Task<bool> CurrentUserAnyOverdueLoans()
{
string curruser = _context.User.Identity.Name;
List<BookLoan.Models.BookStatusViewModel> bookStatusViews =
await MyOnLoanReport();
return bookStatusViews.Any(a => a.Status == "OVERDUE");
}
…
}
The above authorization handler will need to be added to the service collection so that it can be used by the authorization service imperatively:
services.AddSingleton<IAuthorizationHandler, BookLoanAccessHandler>();
In the next section, I will show how to implement a requirement based authorization handler.
Implementation of Requirement-based Authorization Handlers
In the handler BookLoanAgeRestrictionHandler below, we include parameters that include the user context, a policy requirement, and a class resource.
BookLoanAgeRestrictionHandler.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using BookLoan.Authorization;
using BookLoan.Models;
using BookLoan.Services;
namespace BookLoan.Authorization
{
public class BookLoanAgeRestrictionHandler : AuthorizationHandler<MinimumAgeRequirement, BookViewModel>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
MinimumAgeRequirement requirement,
BookViewModel resource)
{
if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
{
return Task.CompletedTask;
}
var dateOfBirth = Convert.ToDateTime(
context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value);
int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
{
calculatedAge--;
}
if (resource.Genre.Contains("Adult"))
{
if (calculatedAge >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
}
else
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}
Below is the policy requirement class definition used in the above authorization hander, which is declared with a parameter for a minimum age restriction so that it can be used within a policy-based authorization handler:
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;
}
}
}
Note that the MinimumAge property within the MinimumAgeRequirement instance within the BookLoanAgeRestrictionHandler handler has already been specified within the requirement for the policy BookLoanAgeRestriction, defined in our startup code in ConfigureServices() earlier.
Before we can inject the above authorization handler within our classes and controllers, we will need to add it to the service collection as shown:
services.AddSingleton<IAuthorizationHandler, BookLoanAgeRestrictionHandler>();
In the next section, I will show how to combine the above authorization handlers to restrict access within a controller method that creates a book loan.
Imperative Authorization based on Policies, Requirements and Resources
In this example we can allow any user that is authenticated to view books based on custom security criteria.
Before we can use imperative authorization, we need to declare IAuthorizationService within our custom class or controller. When we enabled the authorization middleware in the startup with AddAuthorization(), it provided us with the default authorization service, IAuthorizationService, which we can inject into any class to use our authorization handlers imperatively.
Below is a sample simplified LoanController that uses the authorization handler within the code block of the Create(…) class method to check for loan validity when creating a loan for a specified book. I will explain the logic later.
public class LoanController : Controller
{
private readonly ApplicationDbContext _context;
private readonly IAuthorizationService _authorizationService;
ILoanService _loanService;
IBookService _bookService;
IReportService _reportService;
public LoanController(ApplicationDbContext context,
IAuthorizationService authorizationService,
IBookService bookService,
ILoanService loanService,
IReportService reportService)
{
_context = context;
_authorizationService = authorizationService;
_bookService = bookService;
_loanService = loanService;
_reportService = reportService;
}
…
// GET: LoanViewModels/Create
[HttpGet("api/[controller]/Create/{id}")]
public async Task<IActionResult> Create(int id)
{
// use imperative authorisation to check no outstanding overdue loans.
if ((await _authorizationService
.AuthorizeAsync(User, _reportService, new BookLoanRequirement())).Succeeded)
{
BookViewModel bookView = await _bookService.GetBook(id);
if ((await _authorizationService
.AuthorizeAsync(User, bookView, new MinimumAgeRequirement(18))).Succeeded)
{
LoanViewModel lvm = _loanService.CreateNewBookLoan(id);
lvm.LoanedBy = User.Identity.Name;
BookLoan.Views.Loan.CreateModel createModel = new CreateModel(_context);
createModel.LoanViewModel = lvm;
createModel.LoanViewModel.Book = bookView;
return View(createModel);
}
else
{
return new ChallengeResult();
}
}
else
{
return new ChallengeResult();
}
}
…
}
The authorization service calls the extension method, AuthorizeAsync() twice.
For the first time, it matches the authorization handler BookLoanAccessHandler, which has the matching user context, ReportService resource, and a policy requirement BookLoanRequirement. It then calls the authorization handler, which determines if the user has any overdue loans. In this case, the policy requirement is not used in the handler. In this case, the handler is a Resource-based authorization handler.
In the second call to the extension method, AuthorizeAsync() in the inner block, it matches the authorization handler for the policy BookLoanAgeRestriction. which has the matching user context, and a policy requirement MinimumAgeRequirement and BookViewModel resource. It then calls the authorization handler, which determines if the user has any overdue loans.
More information on authorization handlers in .NET Core can be reviewed on the Microsoft ASP.NET Core security site.
In a future post I will show how to apply both imperative and declarative based authorizations to secure your controllers and views.
That is all for today’s post.
I hope this has been a useful post on how to secure your controllers using imperative 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.