In this next post on authorisation in ASP.NET Core, we look at how you can secure resources based on properties of that resource itself.
In a previous post, we saw how you could create a policy that protects a resource based on properties of the user trying to access it. We used Claims-based identity to verify whether they had the appropriate claim values, and granted or denied access based on these as appropriate.
In some cases, it may not be possible to decide whether access is appropriate based on the current user's claims alone. For example, we may allow users to edit documents that they created, but only access a read-only view of documents created by others. In that case, we not only need an authenticated user, we also need to know who created the document we are attempting to access.
In this post I'll show how we can use the AuthorisationService
to take into account the resource we are accessing when determining if a user is authorised to access it.
Previous posts in the authentication/authorisation series:
- Introduction to Authentication with ASP.NET Core
- Exploring the cookie authentication middleware in ASP.NET Core
- A look behind the JWT bearer authentication middleware in ASP.NET Core
- An introduction to OAuth 2.0 using Facebook in ASP.NET Core
- An introduction to OpenID Connect in ASP.NET Core
- Introduction to Authorisation in ASP.NET Core MVC
- Custom authorisation policies and requirements in ASP.NET Core
- Modifying the UI based on user authorisation in ASP.NET Core
Resource-based Authorisation
As an example, we will consider the authorisation policy from a previous post, "CanAccessVIPArea"
, in which we created a policy using a custom AuthorizationRequirement
with multiple AuthorizationHandler
s to determine if you were allowed access to the protected action.
One of the handlers we created was to satisfy the requirement that employees of the Airline's Lounge were allowed to use the VIP area. In order to be able to verify this, we had to provide a fixed string to our policy when it was initially configured:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy(
"CanAccessVIPArea",
policyBuilder => policyBuilder.AddRequirements(
new IsVipRequirement("British Airways"));
});
}
An obvious problem with this is that our policy only works for a single airline. We now need a separate policy for each new Airline, and the 'Lounge' method must secured by the correct policy. This may be acceptable if there are not many airlines, but it is an obvious source of potential errors.
Instead, it seems like a better solution would be able to take into consideration the Lounge that is being accessed when determining whether a particular employee can access it. This is a perfect use case for resource-based authorisation.
Defining the resource
First of all, we will need to define the 'Lounge' resource that we are attempting to protect:
public class Lounge
{
public string AirlineName {get; set;}
public bool IsOpen {get; set;}
public int SeatingCapacity {get; set;}
public int NumberofOccupants {get; set;}
}
This is a fairly self-explanatory example - the Lounge
belongs to the single airline defined in AirlineName
.
Authorising using IAuthorisationService
Now we have a resource, we need some way of passing it to the authorisation handlers. Previously, we decorated our Controllers and Actions with [Authorize("CanAccessVIPArea")]
to declaratively authorise the Action being executed. Unfortunately, we have no way of passing a Lounge
object to the AuthoriseAttribute
. Instead, we will use imperative authorisation by calling the IAuthorisationService
directly.
In the previous post on UI modification I showed how you can inject the IAuthorisationService
into your Views, to dynamically authorise a User for the purpose of hiding inaccessible UI elements. We can use a similar technique in our controllers whenever we need to do resource-based authorisation:
public class VIPLoungeControllerController : Controller
{
private readonly IAuthorizationService _authorizationService;
public VIPLoungeControllerController(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
[HttpGet]
public async Task<IActionResult> ViewTheFancySeatsInTheLounge(int loungeId)
{
// get the lounge object from somewhere
var lounge = LoungeRepository.Find(loungeId);
if (await authorizationService.AuthorizeAsync(User, lounge, "CanAccessVIPArea"))
{
return View();
}
else
{
return new ChallengeResult();
}
}
We use dependency injection to inject an instance of the IAuthorizationService
into our controller for use in our action method. Next we obtain an instance of our resource from somewhere (e.g. loaded from the database based on an id) and provide the Lounge
object as a parameter to AuthorizeAsync
, along with the policy we wish to apply. If the authorisation is successful, we display the View, otherwise we return a ChallengeResult
. The ChallengeResult
can return a 401
or 403
response, depending on the authentication state of the user, which in turn may be captured further down the pipeline and turned into a 302
redirect to the login page. For more details on authentication, check out my previous posts.
Note that we are no longer using the AuthorizeAttribute
on our method; the authorisation is a part of the execution of our action, rather than occurring before it can run.
Updating the AuthorizationPolicy
Seeing as how we have switched to resource-based authorisation, we no longer need to define an Airline name on our AuthorizationRequirement
, or when we configure the policy. We can simplify our requirement to being a simple marker class:
public class IsVipRequirement : IAuthorizationRequirement { }
and update our policy definition accordingly:
services.AddAuthorization(options =>
{
options.AddPolicy(
"CanAccessVIPArea",
policyBuilder => policyBuilder.AddRequirements(
new IsVipRequirement());
});
Resource-based Authorisation Handlers
The last things we need to update are our AuthorizationHandler
s, which can now make use of the provided resource. The only handler from the previous post that needs updating is the IsAirlineEmployeeAuthorizationHandler
, which we can now modify to use the AirlineName
defined on our Lounge
object, instead of being hardcoded to the AuthorizationRequirement
at startup:
public class IsAirlineEmployeeAuthorizationHandler : AuthorizationHandler<IsVipRequirement, Lounge>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
IsVipRequirement requirement
Lounge lounge)
{
if (context.User.HasClaim(claim =>
claim.Type == "EmployeeNumber" && claim.Issuer == lounge.AirlineName))
{
context.Succeed(requirement);
}
return Task.FromResult(0);
}
}
Two things have changed here from our previous implementation. First, we are inheriting from AuthorizationHandler<IsVipRequirement, Lounge>
, instead of AuthorizationHandler<IsVipRequirement>
. This handles extracting the provided resource from the authorisation context. Secondly, the HandleRequirementAsync
method now takes a Lounge
parameter, which the base AuthorizationHandler<,>
automatically provides from the context. We are then free to use the handler in our method to authorise the employee.
Now we have access to the resource object, we could also add handlers to check whether the Lounge
is currently open, and whether it has reached seating capacity, but I'll leave that as an exercise for the dedicated!
When to use resource-based authorisation?
Now you have seen two techniques for performing authorisation - declarative, attribute based authorisation, and imperative, IAuthorisationService
based authorisation - you may be wondering which approach to use and when. I think the simple answer is really to only use resource-based authorisation when you have to.
In our case, with the attribute-based approach, we had to hard-code the name of each airline into our AuthorizationRequirement
, which was not very scalable and in practice meant that couldn't correctly protect our endpoint. In this case resource-based authorisation was pretty much required.
However, moving to the resource-based approach has some downsides. The code in our Action is more complicated and it is less obvious what it is doing. Also, when you use an AuthorizeAttribute
, the authorisation code is run right at the beginning of the pipeline, before all other filters and model binding occurs. In contrast, the whole of the pipeline runs when using resource-based auth, even if it turns out the user is ultimately not authorised. This may not be a problem, but it is something to bear in mind and be aware of when choosing your approach.
In general, your application will probably need to use both techniques, it is just a matter of choosing the correct one for each instance.
Summary
In this post I updated an existing authorisation example to use resource-based authorisation. I showed how to call the IAuthorisationService
to perform authorisation based on a document or resource that is being protected. Finally I updated an AuthorizationHandler
to derive from the generic AuthorizationHandler<,>
to access the resource at runtime.