Do all your endpoints require authentication?
You built an API, where every endpoint requires the user to be authenticated. If you’re working with dotnet core minimal apis you can add the need for authentication by adding .RequireAuthentication() to that specific endpoint.
Below I’ll show you how you can test all endpoints for authentication with just a few lines of test code.
OpenAPI specs
This code assumes that all endpoints are in in your OpenAPI spec, if you know of a way that can discover all endpoints registered in your API. I would love to update the test below. It also means that it requires that you added .OpenApi() to your endpoints.
Since the test below uses an openapi spec, you can also use this quick test on other apis.
TUnit test project
I’m going to show this off in TUnit, I just follow the manual setup. Be sure to create your test project this way. And since TUnit heavily relies on source generation. I suggest you to built the test project without any tests. Be sure to add a dependency on your API project.
Test for authentication
Just a few steps to create this test
1. Create ApiTestFactory class
Using the Microsoft.AspNetCore.Mvc.Testing gives your some options to run your api and to override some of the settings. This part is only relevant if you’ll be testing a dotnet core api where you have a project reference to.
The WebApplicationFactory<T> needs any public type in your Api. I’ve seen people making an empty interface, but you can also use any of the already public types in your api.
using Microsoft.AspNetCore.Mvc.Testing;
...
public class YourApiFactory : WebApplicationFactory<YourApiNamespace.AnyClass>
{
// No other code needed for now,
// you might want to override settings or other things for testing
}
2. Create BluntAuthenticationTests class
Go ahead and create yourself a new class with whatever name you want, I’ll pick BluntAuthenticationTest.
using Microsoft.AspNetCore.Mvc.Testing;
public class BluntAuthenticationTest : IDisposable
{
private readonly YourApiFactory _factory;
public BluntAuthenticationTest()
{
_factory = new YourApiFactory();
}
public void Dispose()
{
_factory.Dispose();
}
....
}
3. Method Data Source
We eventually want a single test that is executed for all endpoints in the specs, let’s create a TUnit compatible method data source. The idea is, load the OpenAPI spec, and enumerate all paths with the supported methods.
public record Endpoint(string Method, string Path);
public static IEnumerable<Func<Endpoint>> GetAllPublicEndpoints()
{
// Because this is a static method, we need to create a new factory instance
var factory = new YourApiFactory();
var httpClient = factory.CreateClient();
// This cannot be an async method because it is used in a MemberData attribute, and TUnit does not support async methods for test data
// Load the known OpenAPI spec endpoint
var response = httpClient.GetAsync("/openapi/v1.json").GetAwaiter().GetResult();
response.EnsureSuccessStatusCode();
var json = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
var doc = System.Text.Json.JsonDocument.Parse(json);
// Enumerate paths and methods, resulting in an enumerable having method and path
var paths = doc.RootElement.GetProperty("paths");
foreach (var pathProp in paths.EnumerateObject())
{
foreach (var methodProp in pathProp.Value.EnumerateObject())
{
// This yield return might look strange but this is the way TUnit wants it.
yield return () => new Endpoint(methodProp.Name.ToUpperInvariant(), pathProp.Name);
}
}
}
4. The test
I recon this test does not need any explanation, apart from it using the MethodDataSource described above
[Test]
[MethodDataSource(nameof(GetAllPublicEndpoints))]
public async Task Enpoint_should_return_401_without_token(Endpoint endpoint, CancellationToken cancellationToken)
{
var client = _factory.CreateClient();
var request = new HttpRequestMessage(new HttpMethod(endpoint.Method), endpoint.Path);
var response = await client.SendAsync(request, cancellationToken);
await Assert.That(response.StatusCode).IsEqualTo(System.Net.HttpStatusCode.Unauthorized);
}
5. Replace ID parameters
The test above did not work because we were using path parameters in the form of {someId}, and our method data source was returning the paths as in the OpenApi spec and the an actual endpoint ID. We start with some compiled regular expressions
using System.Text.RegularExpressions;
// Compiled regex
public static partial class PathParameterExpressions
{
[GeneratedRegex(@"\{[^}]*[Ii]d\}")]
public static partial Regex PathParameterRegex();
}
And the updated test, where all the ids in the paths are replaced with random guids.
[Test]
[MethodDataSource(nameof(GetAllPublicEndpoints))]
public async Task Enpoint_should_return_401_without_token(Endpoint endpoint, CancellationToken cancellationToken)
{
var client = _factory.CreateClient();
// Replace all {*id} path parameters with a new GUID, this should be adjusted based on the actual parameter type
var path = PathParameterExpressions.PathParameterRegex().Replace(endpoint.Path, _ => Guid.NewGuid().ToString());
var request = new HttpRequestMessage(new HttpMethod(endpoint.Method), path);
var response = await client.SendAsync(request, cancellationToken);
await Assert.That(response.StatusCode).IsEqualTo(System.Net.HttpStatusCode.Unauthorized);
}
Conclusion
This pretty simple test might not catch all endpoints (if you did not enable OpenAPI on them), but any endpoints in your OpenAPI specification are automatically checked by this test. We run all our unit tests before every release, and I have to admit this test caught at least two endpoints in a pre-production api that did not have authentication enabled.
One of the things that could be improved is not relying on the OpenAPI specs, but I have no idea how that would work. This is not an advanced check or any thing. It is a blunt way that will probably catch all endpoints where you missed authentication. This test also does not take a long time to run, so there are zero reasons why you should not include this in the test suite of your next api.