When implementing an API using ASP.NET Core, there’s often a need to authorize that API’s users. Your system might be organized into several separate areas, which provide access to different resources and actions. It’s very likely that not all users are allowed to use all of those areas. Within a single area, a design might require restricted access to data entities themselves. There are many ways to implement such authorization, one of them being declaring custom authorization attributes on controller methods/actions or on controllers as a whole.

The permission model

Depending on the design of your application, it could be necessary to develop a permission model that’ll describe what the user is allowed to do in a specific area of the system. Let’s say, for example, that the API has two areas: the administrator area and the buyer area. The administrator of the API will be able to:

  • manage warehouses
  • manage items in the warehouses
  • manage the list of users who can buy from those warehouses.

The buyer of the API will be able to:

  • see the list of warehouses he or she is assigned to
  • see the list of products in a warehouse
  • buy products in a warehouse.

Once we’ve organized the API in this way, we can create a simple object that represents all the possible permissions. How this data is stored in the database can be implemented in different ways, but here we can just assume we’ve got a method to fetch the UserPermissions object for a given user Id.

1ASP NET The permission model

On every API method call which must be protected by our custom authorization, we have to acquire the relevant UserPermissions object. One option is to query the database and build the object on every API call, but that could cause performance issues. Another option is to create the object during user login and serialize it into the JWT token. So on each API call, the client-side will send the UserPermissions object as a part of the token, and it’ll be deserialized in the back-end custom authorization code. Handling the lifetime of the token and, therefore, the lifetime of the UserPermissions object is important in this case.

Custom authorization attributes

The main goal of this text is to build easy-to-use custom attributes that’ll allow us to easily define the requested permissions for API calls. So let’s create a new class: ApiAuthorizationFilter. It’ll be a derived class of ActionFilterAttribute, a class in the Microsoft.AspNetCore.Mvc.Filters namespace, used for executing a code before, or after, controller actions. For our custom authorization, we’ll override the OnActionExecuting method. The constructor will take a parameter which is a list of AdministratorPermissions (enum), representing all the permissions that allow a user to call the method to which the ApiAuthorizationFilter is applied to.

The GetUserPermissions method reads the token and deserializes it into a UserPermissions object.

2ASP NET Custom authorization attributes

3ASP NET Custom authorization attributes

Applying the custom attribute

The custom attribute is now ready to be applied to a controller action. It’s simply declared above the method, with the permissions required for that particular piece of code. And that’s all there is to it.

Before calling the controller action, the OnActionExecuting will be called. The constructor parameter can be used to search through the UserPermissions object provided through the token. In case the permission doesn’t exist, the action is forbidden to run.

If the entire controller requires the same set of permissions, the ApiAuthorizationFilter attribute can be applied above the Controller definition. It’ll be applied to all action methods, and the code will be as tidy as it gets.

5ASP NET Applying the custom attribute

That covers the general principle of how to use method attributes to define custom authorization for controller actions. But we might need to create permissions which are assigned per entity, as in the case of a Buyer trying to get a list of Products in a Warehouse. A Warehouse is identified by its WarehouseId, and that must be a parameter in the method for getting Products.

We’ll add a new constructor for defining the allowed WarehousePermissions in the ApiAuthorizationFilter:

6ASP NET Applying the custom attribute

And we can add the ApiAuthorizationFilter attribute to the controller method for fetching the Products:

7ASP NET Applying the custom attribute

But as WarehousePermissions are attached to specific Warehouses, we need a way to determine which method parameter of GetProducts represents the WarehouseId to be looked for. Attributes don’t have access to the parameters of the method through the constructor, and they’re created at compile-time, so they can only accept predefined objects or constants. On the other hand, the ApiAuthorizationFilter does have access to the ActionExecutingContext object, and it’s possible to look through the parameters inside OnActionExecuting.

So, one possible way to find the required method parameter is to add a string in the ApiAuthorizationFilter constructor and look for the parameter by its name.

8ASP NET Applying the custom attribute9ASP NET Applying the custom attribute

Once we have that, we can then search for the given parameter and use it as a WarehouseId:

10ASP NET Applying the custom attribute

11ASP NET Applying the custom attribute

We can call this method in OnActionExecuting, and use the found WarehouseId to check (CheckPermissionsForValue) whether the UserPermissions object contains a list of WarehousePermissions for that Id. If the list contains any of the permissions, we specified in the attribute constructor, the controller method is allowed to return a result.

12ASP NET Applying the custom attribute

The obvious downside to this approach is using ‘magic strings’ in the attributes, which means there is a typo just waiting to happen. Therefore, it would be nicer to have some more strongly typed manner of finding the WarehouseId in the method parameters.

Parameter attributes

In order to mark a controller action parameter as WarehouseId, we’ll create the custom parameter attribute WarehouseIdAttribute.

13ASP NET Parameter attributes

It doesn’t need any internal logic. It’ll only be used to point out the needed parameter in the method signature.

14ASP NET Parameter attributes

Once we’ve marked the correct parameter in the controller method, we can find it in ApiAuthorizationFilter. And the ‘magic strings’ are gone.

13ASP NET Parameter attributes

Swagger integration

A useful thing for developing an API in ASP.NET Core is using Swagger to easily test the controller methods without actual client-side implementation. Regardless of whether the UserPermissions object is kept in the token or fetched on the back-end, knowing which permissions are needed for which API endpoint makes using the API much simpler. To make the life of the frontend developer a bit easier, we can add some simple adjustments to show the permissions we added using custom attributes.

In the ApiAuthorizationFilter class, a property named PermissionsString is added to create a user-friendly string from the permissions added to the object by the constructor.

We’ll also create a class implementing the IOperationFilter interface, called PermissionsFilter. This IOperationFilter will be used by Swagger to produce the UI.

17ASP NET Swagger integration

The Apply method tries to find a filter in the ApiDescription, which has our custom type (ApiAuthorizationFilter). If one is found, the user-friendly permission string is added to the description for the operation. This PermissionsFilter is then added to the Swagger options, while services are configured.

18ASP NET Swagger integration

This simple adjustment makes the SwaggerUI show the required permissions for every API method.

19ASP NET Swagger integration

Wrap-up

Using custom attributes is a very practical and clean way to implement a custom authorization system for your ASP.NET Core API. It reduces the amount of redundant code and allows developers to make the permissions as granular as needed. By simple adjustments to Swagger (if it’s used), the front-end developers can also easily see which permissions an API method requires.

What's your reaction?