It is common for web applications to have many different endpoints/actions with different authorisation requirements. For example, the API behind this blog only allows users with a sufficient role to create new posts.
In ASP.NET Core applications, the framework provided [Authorize]
attribute allows claims-based authorisation, however this doesn't take into account the resource being accessed. An application may have user-specific resources, be multi-tenant or have some other custom security model. In these cases - authorisation often needs to be granted based on resource-level access.
Imperative-style authorisation using IAuthorizationService
can be used in these circumstances, however with this method we lose the descriptive, declarative simplicity of attribute-based authorisation.
This post provides an example of how we can create custom Action Filters and Attributes, to provide a flexible, efficient and descriptive method of resource authorisation.
Authorisation Rule Attributes
We can use attributes to decorate each endpoint with a rule that's specific to the request content and resource being accessed.
The authorisation rule can be simplified to the following interface:
We can then create an attribute class that implements the interface we have defined:
The example above uses the idArgumentName
value from the constructor to extract the requested id
from context.ActionArguments
. It uses an IItemLookup
and IUserIdProvider
service (both examples, not framework-derived!) from the service provider, to check if the current user has access to the resource with the id
extracted from the request.
The ItemOwner
attribute can then be used to decorate endpoints that require the user to own the resource they are accessing:
With the simple IAuthorisationRule
interface, it's trivial to create multiple implementations of these attributes, each tailored to a specific resource security requirement.
Authorisation Filter
In it's current state, the example above offers no protection as it will not be run as part of the request pipeline. We can use Action Filters to intercept the request, consume our IAuthorisationRule
attributes and check user access before allowing the request to proceed.
Create an Action Filter and register it globally to intercept every request to our application:
Register the filter in Startup
Now that this filter has been registered globally, our earlier example will now become operational:
The ResourceAuthorisationFilter
will now run the HasAccess
method on each of the IAuthorisationRule
type attributes that the action is decorated with. With the current setup, any action not decorated with an IAuthorisationRule
type attribute will not be impacted.
With this basic interface, we can easily extend our collection of rules to provide authorisation logic to check user access against new controller actions, with different resource access requirements. This solution provides a neat, reusable, declarative approach to resource-based authorisation.
Considerations
It's important to consider some of the drawbacks when using this declarative approach. We're accessing some information about the resource before the action body is hit, then it's likely that this resource will be accessed again within the action.
This disconnect between the authorisation filter and the action could be the source of security issues if the resources being accessed are different between the two. Security checks taking place in the authorisation filter must cover all request parameters that can subsequently be used to access resources. In any case - it's good practice to create endpoint integration tests that ensure the rules are correctly applied.
🏁 Summary
Resource-based authorisation in ASP.NET Core applications can be performed in a declarative manner using custom attributes and action filters.
Decorating endpoints/actions with attributes in this way greatly simplifies the implementation of authorisation, making it very easy to apply authorisation rules to new endpoints within the application.
Using a common IAuthorisationRule
attribute interface alongside a global action filter gives developers the flexibility to create authorisation rules that are specific to the security model of the application.
Full source code and an example can be found in this github repo.