Unit Testing Authorization on your Controllers

In many of my ASP.NET web applications (mostly MVC or Web APIs) I have a mix of content with endpoints that either require authentication and authorization or that can be accessed anonymously. For example I have mostly public content (such as my own web site) with a protected management (admin) section. Or I have a Web API project with many endpoints protected by authorization but with a few of them publicly available. To ensure you protect your controllers well, you have many different options. In this article I'll show you a short unit test that you can use to ensure your controllers are properly configured when using a mixed authorization scenario.

One option to configure authorization in .NET Core, is to call RequireAuthorization like this:

app.UseEndpoints(endpoints =>
{
  endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}").RequireAuthorization();
});
which blocks access to the routes for non-authenticated users.

There are many different variations to this with tons of options, including defining policies with fallbacks so everything is protected by default. For more information, checkout these links:

Whether or not you set up global authentication, you can use explicit Authorize and AllowAnonymous attributes on your controllers. For example:

[Authorize]
public class HomeController : Controller
{
  ...
}

With this example you would not block everything by default in Startup but authorize specific controllers only. In this case, that would mean that all your endpoints are publicly available except for the ones marked with the Authorize attribute. Alternatively, you could globally block everything (for example using RequireAuthorization as explained above, or with a global AuthorizeFilter in .NET Core or a global AuthorizeAttribute in earlier versions of .NET before Core) and then selectively open up some controllers or action methods:

[AllowAnonymous]
public class HomeController : Controller
{
  ...
}

For the scenario where not everything is blocked and only specific controllers require authorization, it's a good idea to set up unit tests to ensure the controllers that require authorization have the attribute set. It can be a single unit test that checks all controllers in your project for the Authorize attribute, with an option to exclude certain controllers explicitly. For that to work, you need some code that finds all controller types and then checks them for the presence of the Authorize attribute. To get the controllers, you can write your own reflection code, or make use of an existing library. In many of my projects that have unit tests I use the FluentAssertions library for easy testing my assertions. That library also comes with a handy AllTypes that provides access to the type system in an easy way. So instead of inventing my own wheel, I usually use that class to find all controller types. The full test could look like this:

[TestClass]
public class ControllerTests
{
  [TestMethod]
  public void AllControllersShouldImplementAuthorizeAttribute()
  {
    var excludedTypes = new[]
    {
      typeof(HomeController)
    };
    var assembly = Assembly.GetAssembly(typeof(HomeController));
    var allControllers = AllTypes.From(assembly).ThatDeriveFrom<Controller>().Except(excludedTypes);
    var controllersWithoutAuthorizeAttribute = allControllers.Where(t => !t.IsDefined(typeof(AuthorizeAttribute), false)).ToList();
    var brokenControllers = string.Join(" and ", controllersWithoutAuthorizeAttribute.Select(x => x.Name));
    controllersWithoutAuthorizeAttribute.Count.Should().Be(0, "because {0} should have the Authorize attribute", brokenControllers);
  }
}    

With this unit test in place, imagine I have two controllers: HomeController (excluded in the list above) and SecondController:

public class HomeController : Controller
{
}

public class SecondController : Controller
{
}  

If I run the test now, my test output will look like this (I run my tests with ReSharper so that's what you see in the screenshot below, but other test runners will show similar output):

ReSharper's test runner showing that the test has failed because SecondController doesn't have the Authorize attribute

If you add the Authorize attribute to SecondController or add it the exclusions list, the test will pass:

ReSharper's test runner showing that the test has now passed

You can extend this code to support other scenarios such as controllers in multiple projects (and thus attributes) and other configured default behavior for authentication in Startup.

Note: although this snippet is placed in the ASP.NET Core category and the authorization configuration code targets Core as well, you can use the same unit test code and FluentAssertions library on your ASP.NET Framework applications.

Happy testing!


Where to Next?

Wonder where to go next? You can read existing comments below or you can post a comment yourself on this article.

Doc ID 616
Full URL https://imar.spaanjaars.com/616/unit-testing-authorization-on-your-controllers
Short cut https://imar.spaanjaars.com/616/
Written by Imar Spaanjaars
Date Posted 11/15/2020 14:26

Talk Back! Comment on Imar.Spaanjaars.Com

I am interested in what you have to say about this article. Feel free to post any comments, remarks or questions you may have about this article. The Talk Back feature is not meant for technical questions that are not directly related to this article. So, a post like "Hey, can you tell me how I can upload files to a MySQL database in PHP?" is likely to be removed. Also spam and unrealistic job offers will be deleted immediately.

When you post a comment, you have to provide your name and the comment. Your e-mail address is optional and you only need to provide it if you want me to contact you. It will not be displayed along with your comment. I got sick and tired of the comment spam I was receiving, so I have protected this page with a simple calculation exercise. This means that if you want to leave a comment, you'll need to complete the calculation before you hit the Post Comment button.

If you want to object to a comment made by another visitor, be sure to contact me and I'll look into it ASAP. Don't forget to mention the page link, or the Doc ID of the document.

(Plain text only; no HTML or code that looks like HTML or XML. In other words, don't use < and >. Also no links allowed.