Securing ASP.NET Apps With Global Authorization Filters

Mar 2022

Many web applications I build end up with the following security need:

  • 99% of requests need to be authenticated
  • 1% of requests don't

Seeing this need, many applications end up like this:

  • 99% of controllers/routes have [Authorize] attributes on them
  • 1% of them don't

Issues with this setup

Sure, this gets the job done. You can ship your application and everything might be secured. But let's think through the implications of building the authentication this way.

Security Implications if an [Authorize] tag is missed

Let's imagine you're building an invoicing system. You've been working long hours to get the job done. As you're pushing towards the finish line, you get a little careless and forget to add an authorize tag on the GET endpoint that returns a list of all invoices in the system. You test everything and it appears to work great! Let's ship it.

Well, unbeknownst to you, you just exposed all of the invoices to the world. Woops. This is something that can go unnoticed if no one is specifically reviewing for this type of flaw. Without some type of security review to make sure every endpoint is secured, this could easily slip by for a long time.

Extra clutter adding the [Authorize] tags

Let's say you have 20 controllers, with 4 endpoints in each controller. That's at least 20 lines of just [Authorize]. If you're setting it on every endpoint, that's 80. Yikes. This creates unnecessary noise as someone reads through the code.

A better way

Let's flip our authentication thinking on its head. By adding [Authorize] attributes on every endpoint, we are essentially saying “everything is unauthenticated, unless I say otherwise. What if we flipped that around and said “everything is authenticated, unless I say otherwise”. We enforce the authentication requirement by default. This eliminates the need for us to explicitly say that our endpoint is authenticated every time we build one. This also eliminates the risk that we forget to mark an endpoint as authenticated.

Using a global authorize filter

To implement this change, we can leverage a global authorization filter. In our Program.cs application bootstrapping code, we can specify a filter that requires all requests to be authenticated.

This example is in .NET 6, but a similar solution can be created in older versions of .NET Core using the Startup.cs file.

Adding this code will require every request to be authenticated. Pretty simple. This does not enforce any specific role requirement. You can add additional [Authorize] tags with specific role requirements throughout the application to narrow down the users allowed to access specific areas. You can also add a base level role requirement for all requests by using the .RequireRole() method on the policy builder.

I have found this useful when building applications that leverage a shared identity store such as azure AD. If we want to restrict access within an organization to only users who are in a specific security group, we can use .RequireRole() to require that base access level.

Note: These examples exclude the authentication setup itself, e.g. setting up azure AD or ASP.NET identity integrations.

But what if we need to make some requests anonymous?

No problem. Simply add the [AllowAnonymous] attribute to any controllers or endpoints that need to be open. Places like login pages or user registration typically end up getting these.

. . .

Wrap Up

When it comes to authentication and authorization, there are many ways to implement it. By using a global authorization filter, we can save ourselves the headaches of adding [Authorize] attributes all over our controllers, as well as ensuring every endpoint is protected by default.

{}*
Tags
Technical
ASP.NET
Security