Nice to knows when implementing policy-based authorization in .NET
I am writing this post to share my experience after I encountered a couple of unforeseen behaviors while implementing a policy-based authorization layer in .NET. I hope this post helps you avoid the same "pitfalls", or at least raise some awareness about this topic.
Reflecting back, this was due to my lack of understanding of how policy-based authorization works in .NET. Instead of getting a deep understanding of the topic, I made some assumptions, which were wrong (don't just assume, but test and verify!). The Policy Authorization documentation does a good job explaining the concepts, and covers most of the scenarios you might encounter. I recommend you read it before writing your own policies so you don't make the same mistakes I did.
Getting this right is critical, as it's the first line of defense against unauthorized access to your application. What's why it's ranked first on the OWASP TOP 10 list, A01:2021 – Broken Access Control A01 Broken Access Control.
Recap: Policies in ASP.NET link
In ASP.NET, policies are used to define requirements that must be met in order to give a user the authorization to access an endpoint.
Within Web API projects you can use policies to guard and protect endpoints from unauthorized users that don't meet the criteria. Instead of writing the authorization logic in the endpoint itself, you can refactor this into a policy that is reusable across multiple endpoints. By extracting the authorization logic from the endpoint, you can make your code more clear and keep your endpoints concise.
To register a policy:
- Add the
AddAuthorization()
middleware; - Use the
AddPolicy()
method to define a policy with its requirements; - Register the authorization handler(s) to the DI container;
- Enable the authorization middleware in the application by invoking
UseAuthorization()
; - Guard your endpoint with the policy by using
RequireAuthorization()
for Minimal APIs, for controllers use the[Authorize]
attribute;
Create a new class that inherits from AuthorizationHandler<TRequirement>
, you will have to implement the HandleRequirementAsync
method, which's responsibility is evaluating the request against the requirement.
In the HandleRequirementAsync
method, you can implement the authorization logic and use the passed in AuthorizationHandlerContext
object to flag the requirement as (un)met.
- Use the
context.Succeed(requirement)
method to indicate that the requirement is met; - Use the
context.Fail()
method to indicate that the requirement is not met; - Don't invoke the
context.Fail()
method when the requirement to let another handler evaluate the requirement (more on this later);
The requirement in this case is a simple class that implements the IAuthorizationRequirement
interface.
For more complex requirements you can add properties to the requirement class, which can be used to configure the requirement.
Nice to knows link
All policies (handlers) are always evaluated link
This was the biggest surprise to me.
I expected that when a requirement is not fulfilled that it would short-circuit the process and stop the evaluation of the other policy handlers. But, this is not the case.
When multiple policies are applied to an endpoint, all of them are evaluated regardless of the outcome of the previous policies. The same scenario applies when a requirement is validated by multiple handlers (yes, you can have a requirement that is validated by more than one handler - more on this later).
I was interested in this decision, and it turns out that this is by design as it allows policy handlers to execute side-effects, such as logging. This can be crucial information for auditing purposes.
If your use case is different you can opt out of this behavior and stop the evaluation of the other policies on a first failure. To do so, set the InvokeHandlersAfterFailure
property to false
in the AuthorizationOptions
configuration. This will stop the evaluation of the remaining policies after the first failure.
Be aware that this only takes place when a policy handler explicitly invokes the context.Fail()
method.
A handler that does not invoke context.Succeed()
, and simply returns, is not sufficient to stop the evaluation of the other policies.
See the implementation of DefaultAuthorizationService to understand how policies are evaluated.
Example link
In the following example, the two policies PolicyName
and OtherPolicyName
require a different policy requirement, and is implemented by two different handlers.
These handlers are always evaluated regardless of the outcome of the other policy.
The order of execution is not guaranteed link
The order in which the policies are evaluated cannot be guaranteed. You cannot safely rely on the order of the policies when implementing your authorization logic.
A handler should be implemented as stand alone and should be written in a way that does not depend on the order of the policies.
If for some reason you want to know which policies are still in the pipeline, you can use the PendingRequirements
property of the AuthorizationHandlerContext
object.
Unauthenticated requests are also evaluated by policies link
But what happens if the user is not authenticated?
To understand this behavior we first need to know how ASP.NET deals with (un)authenticated requests.
Using the RequireAuthorization()
method (in Minimal APIs) or the Authorize
attribute (in controllers), adds the DenyAnonymousAuthorizationRequirement
requirement to the authorization pipeline.
With the knowledge that all policies are always evaluated and the insights cannot safely rely on the order of the policies, we can conclude that our policy handlers are also called even when the user is not authenticated.
As a solution, always check if the user is authenticated within your policy handlers.
For some inspiration, take a look at how DenyAnonymousAuthorizationRequirement
is implemented.
Example link
In the following example, the endpoint is protected with the default RequireAuthorization()
, which means that the user must be authenticated.
Additionally, the endpoint is also protected by the policy PolicyName
, using RequireAuthorization("PolicyName")
.
The policy handler of PolicyName
is evaluated regardless if the user is authenticated.
Difference between FallbackPolicy
and DefaultPolicy
link
While configuring the authorization options, you might have noticed the FallbackPolicy
and DefaultPolicy
properties.
While they might sound similar, they have different purposes.
- The
FallbackPolicy
is used when an endpoint does not have any policies applied to it. - The
DefaultPolicy
sets the default policy for an endpoint that uses theAuthorize
attribute or theRequireAuthorization()
method, but does not specify any policies.
For more information about the differences between the two, see the blog post Globally Require Authenticated Users By Default Using Fallback Policies in ASP.NET Core by Scott Sauber.
Multiple handlers for the same requirement link
We touched on this topic earlier, but it's worth going into more detail. When multiple handlers that check an identical requirement are registered, these handlers are all executed.
For the requirement to be met, it's sufficient that one of the handlers succeeds. The same applies when a handler fails, the requirement is not met.
This makes it important to how handlers handle unsuccessful authorization attempts.
I suggest using the context.Fail()
method in exceptional cases only. Use it only to enforce a strict rule that must be met. In other all other cases simply do not invoke the context.Succeed()
. This makes it flexible for other handlers to evaluate the requirement.
The example in the documentation shows a good example of how to handle multiple handlers for the same requirement. It uses a building entry system as an example, where the requirement is to have a valid badge to enter the building. If an employee forgets the badge, she can still enter the building by making use of a temporary badge.
Because the primary handler (verify the badge) does not call the context.Fail()
method, the secondary handler (temporary badge) can still evaluate the requirement and give the user access to the building.
Example link
In the example below, the endpoint is protected by a single policy.
However, that policy is implemented by two authorization handlers (handling an identical requirement).
The endpoint is only accessible when one of the policies invokes context.Succeed()
and the other does not invoke context.Fail()
.
The endpoint is not accessible in the following scenarios:
- One of the policies invokes
context.Fail()
; - None of the policies do not invoke
context.Succeed()
;
Authorization handler that checks multiple requirements link
So far we've only seen authorization handlers that check a single requirement.
For use cases where a handler needs to check multiple requirements, you can create a handler that implements the IAuthorizationHandler
interface.
Using the context have access to all the pending requirements using PendingRequirements
, and can evaluate them accordingly.
This can be useful when you want to have some more control over the authorization flow, for example, "OR" logic can easier be implemented.
Resource-based authorization link
For the cases when you want to check if the user has access to a specific resource there's the IAuthorizationService
service.
Instead of marking an endpoint using RequireAuthorization()
(or the [Authorize]
attribute), you can manually invoke the authorization flow.
In fact, you can also replace the RequireAuthorization()
with the IAuthorizationService
service for the previous examples.
The reworked example injects the IAuthorizationService
service and invokes the AuthorizeAsync()
method to check if the user has enough permissions to access the endpoint.
The AuthorizeAsync
method accepts the user and the policy name as arguments and returns an AuthorizationResult
object.
This approach doesn't bring much value in this case, but it can be useful when you want to check the authorization of a specific resource.
AuthorizeAsync
additionally accepts a resource object. This resource object can be used to check if the user has access to that resource.
Why is this useful? This technique can be leveraged to avoid multiple roundtrips to the database. Instead of retrieving a resource in the policy handler and in the endpoint, you can retrieve the resource in the endpoint and pass it to the policy handler.
The authorization handler that is configured to handle the policy can also accept a second parameter, which is the resource object. In this case, the retrieved document is passed to the handler.
Register the handler with the DI container and configure the policy is done the same way as before.
Conclusion link
In this blog post, we went through the basics of policy-based authorization in .NET.
Why it's useful, and how to implement a policy-based authorization layer in your application using requirements (IAuthorizationRequirement
) and authorization handlers (AuthorizationHandler
).
The key takeaways when building your own policies are:
- If a policy requirement is not satisfied, the other policies are still evaluated (this also applies when the user is not authenticated)
- Policies are evaluated in an unpredictable order, do not rely on the order of the policies or authorization handlers
- It's possible to write multiple authorization handlers for the same requirement. Here it's sufficient that one of the handlers succeeds for the requirement to be met. Invoke
context.Fail()
in one of the handlers only when you want to enforce a strict rule, in other cases simply do nothing when the requirement is not met (in this scenario a different handler can verify the requirement). - A single authorization handler (using
IAuthorizationHandler
) can check multiple requirements (can be used to implement anAND
orOR
logic) at once. - For resource-based authorization leverage the
IAuthorizationService
service to manually invoke the authorization flow to avoid multiple roundtrips to the database (performance matters!).
Feel free to update this blog post on GitHub, thanks in advance!
Join My Newsletter (WIP)
Join my weekly newsletter to receive my latest blog posts and bits, directly in your inbox.
Support me
I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.