Never forget the Authorize attribute
Did you ever deploy an application and forgot to add the [Authorize]
attribute to a controller or action? I did, and it meant that the application was open to the public and one controller could be used without logging in. Let me show you how you can use reflection and Fluent Assertions to check for the existence of an attribute on a specific class. I’m using this to verify the Authorize
attribute on all classed that inherit from ControllerBase
in a .NET Core application, but this principle can be used on any class to check for the existence of a specific attribute.
Create a new test project
I picked xUnit as my test framework, but similar techniques might be available in other test frameworks as well.
dotnet new xunit -n MyProject.Tests
# This will create a new test project in the folder MyProject.Tests
# Go to the test project folder
cd MyProject.Tests
# Add a reference to the project you want to test
dotnet add reference ../MyProject/MyProject.csproj
# Add a reference to the Fluent Assertions library
dotnet add package FluentAssertions
Create the test
Open your project in your favorite C# editor and create a new file in the MyProject.Tests
folder called AuthorizeAttributeTests.cs
. Add the following code to the file:
using FluentAssertions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace MyProject.Tests;
public class AuthorizationTests
{
public static IEnumerable<object[]> GetControllers() {
// Get all controllers in assembly
// This code uses 1 class in the referenced project to get all the controllers in that assembly
// `Controllers.SomeController` can be replaced with any class in the project that contains the controllers
var controllers = typeof(Controllers.SomeController).Assembly.GetTypes()
.Where(type => typeof(ControllerBase).IsAssignableFrom(type))
.Select(type => new object[] { type });
return controllers;
}
[Theory]
[MemberData(nameof(GetControllers))]
public void Controller_should_be_decorated_with_authorize_attribute(Type controllerType)
{
controllerType
.Should()
.BeDecoratedWith<AuthorizeAttribute>("because all controllers should be protected.");
}
}
Explanation
The Theory
attribute on the test itself is used to tell xUnit that this these has inputs. The MemberData
attribute is used to let xUnit know that the GetControllers
method should be used to get the data for the test. If you want more details on the MemberData
attribute, you can read more about it here, in an awesome blog post by Andrew Lock.
The Controller_should_be_decorated_with_authorize_attribute
test is executed once for each item in the MemberData
. The controllerType
parameter is the actual type of the controller that is being tested. The BeDecoratedWith
method from Fluent Assertions is used to check if the Authorize
attribute is present on the controller. If the Authorize
attribute is not present, the test will fail.
Over at the website of Fluent Assertions, you can find many more examples on this topic.
The GetControllers
method uses reflection to get all the types in the assembly that inherit from ControllerBase
.
Future improvements
- This test can be expanded to check for the
Authorize
attribute on all actions in the controllers as well. This can be done by using reflection to get all the methods in the controller and check for theAuthorize
attribute on each method. - Allow a developer to just specify the
AllowAnonymous
attribute on a controller or action if they want to allow anonymous access to a specific controller or action, and then skip all controllers that have this attribute specified.
Implementing these improvements might be a nice exercise for you, if you want to get started with reflection and attributes in C#. They have good examples to get you started.
Conclusion
This super simple test shows you how you can verify attributes on all classes that inherit from a specific class. In this sample it’s to make sure our application will always have explicit authorization defined on each controller. This makes sure we will never release a version of this application where some endpoints say unprotected unintentionally. We run all tests as part of our build & deployment pipeline, so we will catch mistakes like this as early as possible.
This might turn into a series of short posts about security in .NET Core, so stay tuned for more security shorts.