What is authorization?
The need for authentication and authorization in web application development is more or less mandatory today. Although you can implement both in different ways, there are some .NET Core framework interfaces and services worth looking at and building your mechanism around.
While authentication verifies who you are, authorization is the process that allows you to access a specific resource. While both should be treated equally when it comes to security, we have often seen authorization pushed aside for the following reasons:
– we do not have time for it now
– we can do it later
– we will do it along the way
These statements are red flags and should be eliminated as soon as they are thought or spoken. Somehow it seems that authentication takes precedence over authorization, that your app is already secure enough. The probable reason for this is that authentication is something that is visually perceived. We have all seen many login forms – the user enters their email/username and password and is allowed to “go through”.
Authorization, on the other hand, is perceived as something in the background that readily grants or denies access.
Authorization would not be necessary if you could fully trust the user, but you cannot rely on that.
In general, it is easier to implement an authentication mechanism than authorization because authorization is tied to the security of the whole system and involves many roles in the organization to get it right.
Even if you are not developing large enterprise applications, you should keep your security concerns in mind from the beginning and create a sustainable architecture for your team, your company, your client and yourself.
Authorization as an integral part of the application
Authorization should be considered an integral part of the application, side by side with authentication, logging, localization, testing, etc.
Discussions about authorization should take place in the early stages of development in consultation with project managers and business analysts. The core functionality of an application is closely linked to user access to meant functionality.
Unfortunately, clients usually skip all conversations about this because it is more important for them to see something on the screen.
The examples presented here are based on the ASP.NET Core 6 API project and consider the three-layer architecture approach: API layer, business layer and database layer, but it can be adapted to different implementations as needed.
Project structure
In most cases, authorization checks are performed in the ‘core (business) layer of the application. If you move the authorization logic to the same layer as the API, you have additional benefits in terms of less coupling and better testability, focusing on security concerns in your tests, and cleaner code overall.
If you ask yourself one question, “What do we want to achieve with user authorization?”, the answer is pretty obvious: to restrict users from accessing resources or starting operations if conditions are not met.
If you try to adapt this to your project architecture, you will find that these conditions can and should be checked before even starting the required operation thus extracting them outside of the core layer and performing checks when the API endpoint is entered.
Let’s say we have the following project structure:
When you bring the authentication and authorization logic into the Application.API.Security project, the Application.API.Core is left to do what it has to do – the business logic. This layer should not care if the user is allowed to do something. If the user is allowed to do something – let him.
In the Application.API.Security project we will define Requirements, Handlers and Policies that monitor endpoints within the API project and reject users immediately.
The actors in the authorization process
After all of the above being said, we can name the main actors in the authorization process:
- User – who performs an action. A user is someone who has been successfully authenticated and is allowed to perform certain actions. In general, we are interested in the Claims property of the ClaimsPrincipal object – our authenticated user. Claims contain specific data tied to the user and their organization, such as Id, TenantId, OrgnaizationId, and so on. Claims should contain as little information as necessary.
- Policy – what action is about to perform. Policies are one of the building blocks of the authorization mechanism in ASP.NET Core. They describe what action is to be performed and contain several requirements, all of which must be met. Simply put: one action – one policy.
- Context – what data is used to perform an action. This is the data that will be challenged against the users data. It is a placeholder for values that are sent in the model to the controller who needs to check them. Including this data provides flexibility as you can combine values from the body of the request, the route, the query string, the headers, etc. and then send them for authorization checking.
Building blocks of authorization in ASP.NET Core
IAuthorizationService
This interface is the main point of interest where we check whether the authorization was successful or not, as you can see in the previous example.
Although there are 5 overloads of this method, we are specifically interested in one that contains all the previously mentioned components to provide us with everything we need to have as granular an authorization process as necessary.
IAuthorizationRequirement
The implementation of this interface is just a placeholder to have a strongly typed requirement that will be validated in the authorization handler. It has no body or logic of its own.
IAuthorizationHandler
Here’s where it gets tricky. AuthorizationHandler is an abstract class that contains an abstract HandleRequirementAsync method waiting to be implemented.
AuthorizationHandler has two generic parameters: So you either implement the handler for the requirement only or you implement the handler requirement bound to the resource context of authorization.
IAuthorizationPolicy
Authorization policy can contain one or more requirements.
If all requirements are met, the authorization is successful.
We use policies to describe actions to be performed for specific routes, actions or endpoints. The general idea is that we create a policy for each endpoint/action to be performed.
For example, our ApproveBlogPostPolicy policy handles the requirements to approve the blog post when the endpoint is executed.
We check all the necessary request data from the URL, body or query string and compare it to the data that the user has in its cookie or token.
You can use the static class as a wrapper for all policies related to a specific point of interest, like BlogPosts here for example as policies are resolved by their name (string).
Authorization as a „fail-fast“ principle
As mentioned earlier, we want to make authorization as soon as possible without it intruding into the core, business part of the application. Therefore, authorization checks are the first thing to be performed as soon as a route is executed on the controller.
As you can see, we’ do not use the [Authorize] attribute because it is pretty limited in such a way that you cannot pass runtime values to it, blogging in the following example.
If you are not comfortable with this approach because it is not consistent with the “thin controllers” approach, remember that “thin controllers” does not mean that you only need one or two lines of code per controller. This approach is used to prevent business logic code from entering the controllers, which is not the case here. We just authorize the user and let them go through the pipeline or deny them access.
Requirements
Some basic requirements that are widely used and adopted fall into three main categories: to check ownership, roles and permissions for the user.
Ownership
By ownership requirement, we mean checking whether the topmost entity in our system (Account/Tenant) of the entity being in authorization context (BlogPost for example) belongs to the specified User.
Let’s say we have the following structure: Account > Blogs > BlogPosts > BlogPostPhoto
By checking ownership for CreateBlogPostPolicy for example, you need to make sure that TenantId in the user’s claims matches the TenantId of the BlogPost’s Blog entity Id you are trying to create.
The ownership handler for reading the BlogPost would look something like this:
Role
Below are some examples of how roles are defined on requirement, handler, and in a specific policy. In the following example, roles are treated as enums.
First, we create a RoleRequirement that accepts the RoleEnum as a parameter that is checked in the RoleHandler.
RoleHandler looks for a claim Role where the user’s role is defined and then compares it with the role provided in RoleRequirement.
ApproveBlogPostPolicy guards a specific endpoint and here we define the requirements needed for successful authorization one of which is RoleRequirement defined in the previous step.
Permissions
PermissionsRequirement can be used if you want a more granular approach to authorized users. Although some permissions can be “bundled” within the role, you can define the requirements for each permission if you want to be more specific.
Permissions can normally be saved:
- In the database model, but remember that additional access to the database is required for checks in the authorization process,
- In claims where each permission can be claimed by itself,
- In claims where you bundle a JSON object with all permission flags as true/false.
If you can handle a group of permissions with RoleRequirement then you don’t need to use of PermissionRequirement.
Example of PermissionRequirement and corresponding PermissionHandler:
We can utilize them now inside our policy:
An alternative solution to the authorization stack structure
Another approach is to create a policy for each action to be performed, then create a single requirement and give it a generic name like AuthorizationContextRequirement and create a PolicyConetxt object for each handler as before. This way you can do all the checks for ownership, roles, and permissions in one handler. So, all your policies will have the same one and only one requirement, but the implementation will be different in each of the requirement’s handlers.
Don’t reinvent the wheel – use framework interfaces
If you do not see the need to develop your own mechanisms for authorization, you should stick to the mechanisms provided by the framework and described above. This way, your solution will be cleaner and more understandable to a wider range of developers working on the project and to those who join later.
Some drawbacks to using this approach
Almost every decision you have to make when developing an application is some kind of trade-off between simplicity, performance, cleanliness, etc.
The downside for some here might be that you have to go to the database twice. Once for authorization check when you compare users, context entities and primary and foreign keys. And then later when access is granted to get the resource with the details to be worked on.
Another disadvantage could be the creation of a class for each AuthroizationRequirementHandler of a specific context model, but in my opinion it is just more work to keep everything clean and tidy.
Best practices for effective authorization
Think about security concerns early on when starting a new project – work together with clients, project managers and business analysts on an issue.
Keep your authorization logic close to the API endpoints – do not pollute the business layer and logic.
Use framework defaults for your authorization requirements. This way, others working on a project or joining later on will already be familiar with it if the foundation is built on standards.
Even if you do not use framework-specified interfaces given by the framework, place authorization at the “front door” and do not spread it randomly throughout the business logic or other parts of the code.