If you read this, you’ll learn how to create authorisation, based on a policy. Specifically, they use the example of the user’s age, and create a restriction to say that only users over the age of 21 can access a resource.
The age was something that they got from a claim, stored against the user. But what if your requirement is a little more complex? For example, what if you have a situation, such as a popular question and answer site that you may recognise, where you are trying to restrict access to a resource based on something that can change during the user session. In this post, I’m covering how you can follow the same policy structure, but force the program to go back to the DB to get details about the user, each time they try to access the restricted resource.
As a background, the particular use case here is that a user of my site can approve something, but only where they have a sufficient rating.
Basic Set-up
The first thing that you’ll need is a custom Authorization Handler. For my project, I’ve created a sub directory called Authorization (I’ve even spelled it wrong to be consistent):
The Authorization Handler inherits from an abstract class, which forces you to override a single method: HandleRequirementAsync. Essentially, you tell the handler about your requirement (we’ll come to the requirement next), and then it passes this back to you in the method; let’s have a look at some code:
public class ApproverAuthHandler : AuthorizationHandler<ApproverAuthRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ApproverAuthRequirement requirement)
{
The Requirement is just a class to hold the relevant data that you need. In our example, the requirement would hold the user’s rating; here’s the requirement code:
public class ApproverAuthRequirement : IAuthorizationRequirement
{
public int UserRating { get; set; }
public ApproverAuthRequirement(int userRating)
{
UserRating = userRating;
}
}
IAuthorizationRequirment is what is known as a marker interface. To be honest, I hadn’t come across the term before, but I have used the pattern before. Essentially, this is an empty interface: its purpose it to allow you to pass the class type as a strongly typed interface; but in reality, the class can me anything.
Anyway, let’s get back to the Authorization Handler. Inside HandlerRequirementAsync, you can do anything you choose; should your check be successful, you call context.Succeed(requirement), otherwise, do nothing. In the above linked code, they check a claim against the requirement; we’re going to just get some information from the DB, and check that against the requirement:
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ApproverAuthRequirement requirement)
{
int? userRating = \_userService.GetSubjectRating(context.User);
if (!userRating.HasValue)
{
return Task.CompletedTask;
}
if (userRating >= requirement.UserRating)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
User service is simply a service that calls into a repository, and returns the user rating.
Unit Testing
All well and good, and this looks eminently testable. In fact, it is: you simply mock out the service, and the test looks like this:
// Arrange
var requirements = new[] { new ApproverAuthRequirement(100) };
var user = new ClaimsPrincipal(
new ClaimsIdentity(
new Claim[] { },
"Basic")
);
var userService = Substitute.For<IUserService>();
userService.GetSubjectRating(Arg.Any<IPrincipal>()).Returns(100);
var context = new AuthorizationHandlerContext(requirements, user, null);
var sut = new ApproverAuthHandler(userService);
// Act
await sut.HandleAsync(context);
// Assert
Assert.True(context.HasSucceeded);
No doubt, you could check for the exact service principal under test, although you’re testing the Authorization Handler, so I think this is sufficient.
Resource Based Handler
In this article, there is a similar process discussed; it warrants some further investigation on my part, but I don’t currently see any real difference between my implementation, and this abstraction (although, admittedly, this version looks easier to test).
References
https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-3.1
https://stackoverflow.com/questions/51272610/unit-test-authorizationhandler