How To Deal With Identity When Testing An ASP.NET Core Application


User identity is a collection of security information associated to an authenticated user. It is used to implement authorization mechanisms with the aim of protecting application resources from unauthorized accesses.
In ASP.NET Core, user identity and the related authorizations resolutions are performed through high level middlewares.

Middlewares are elements of the application pipeline through which requests and responses pass to apply a specific logic. They are intended to design multi-layer requests/responses handling systems.

Here is a middleware pipeline schema from the ASP.NET Core Microsoft documentation :
Creating a middleware pipeline with IApplicationBuilder

My serie about Testing ASP.NET Core Applications has started here and several subjects have been covered like Behavior Approach, Configuration, Hosting, Model Testing, Dependencies Mocking, etc. But what about Identity and how to deal with authentication and autorization processes when testing an ASP.NET Core application?

Creation Of A Test Authentication Middleware

As I said before, in ASP.NET Core, User Identity and the relatives authentication systems are implemented through middlewares with a dedicated object model. We can so use this object model to develop our own Authentication Middleware to mock the standard one.

All authentication middlewares in ASP.NET Core have to define a set of options and a handler which are respectively represented by the AuthenticationOptions and the AuthenticationHandler classes.

Authentication Option

Let's here configure our authentication middleware for tests purposes:

public class TestAuthenticationOptions : AuthenticationOptions
{
	public virtual ClaimsIdentity Identity { get; } = new ClaimsIdentity(new Claim[]
	{
		new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", Guid.NewGuid().ToString()),
		new Claim("http://schemas.microsoft.com/identity/claims/tenantid", "test"),
		new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", Guid.NewGuid().ToString()),
		new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", "test"),
		new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", "test"),
		new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", "test"),
	}, "test");

	public TestAuthenticationOptions()
	{
		this.AuthenticationScheme = "TestAuthenticationMiddleware";
		this.AutomaticAuthenticate = true;
	}
}

The ClaimsIdentity property will hold the identity information we want to test within a scenario test. Here, we have defined a set of claims which represent the information we want to link to the test identity. Feel free to add or remove one following your needs.
I have defined this property as virtual to be able to override Identity information in order to adapt the test case.
We have also activated the AutomaticAuthenticate property because we want our middleware to modify the request automatically.

Authentication Handler

The Authentication Handler provides the logic to execute while handling an authentication. For our needs, it will be very simple: the handler will always return an authentication success result containing the user identity holded by the options we have previously defined:

public class TestAuthenticationHandler : AuthenticationHandler<TestAuthenticationOptions>
{
    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var authenticationTicket = new AuthenticationTicket(
                                         new ClaimsPrincipal(Options.Identity),
                                         new AuthenticationProperties(),
                                         this.Options.AuthenticationScheme);

        return Task.FromResult(AuthenticateResult.Success(authenticationTicket));
    }
}
Authentication middleware

The authentication middleware will simply define how to handle a request in a test context using our test authentication handler:

public class TestAuthenticationMiddleware : AuthenticationMiddleware<TestAuthenticationOptions>
{
    private readonly RequestDelegate next;

    public TestAuthenticationMiddleware(RequestDelegate next, IOptions<TestAuthenticationOptions> options, ILoggerFactory loggerFactory)
        : base(next, options, loggerFactory, UrlEncoder.Default)
    {
        this.next = next;
    }

    protected override AuthenticationHandler<TestAuthenticationOptions> CreateHandler()
    {
        return new TestAuthenticationHandler();
    }
}

Application Configuration

The configuration of an ASP.NET Core application is defined within the Startup class. Obviously, we only have one Startup class and we also want to only use our TestAuthenticationMiddleware while running tests.
So inside of the Startup's Configure method, let's use the EnvironmentName configuration property of the hosting environment to parameter the middleware configuration:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    if (env.EnvironmentName == "Test")
    {
        app.UseMiddleware<TestAuthenticationMiddleware>();
    }
    ...
}

Test With Identity

We can now write this kind of test that will be ran using the previously configured identity (please note the use of the UseEnvironment method so that our previous condition is evaluated to true and our TestAuthenticationMiddleWare is used):

[TestClass]
public class BlogTest
{
    [TestMethod]
    public void TestBlog()
    {
        Data[] data = CreateTestScenarioDataIntoInMemoryDb();
        
        var webHostBuilder = new WebHostBuilder().UseEnvironment("Test").UseStartup<Startup>()
        var testServer = new TestServer(webHostBuilder);

        var response = testServer.CreateClient().GetAsync("/api/data").Result;
        response.EnsureSuccessStatusCode();

        var results = response.Content.ReadAsAsync<Data[]>().Result;
        for (int i = 0; i < data.Length; i++)
        {
            Assert.AreEqual(data[i], result[i]);
        }
    }
}

If you want more information about ASP.NET Core Testing, please take a look at my previous posts:

And feel free to contact me ;)

Arnaud

Arnaud

I am a Developer/Tech-Lead freelance. Mainly focused on Mobile, Web and Cloud development, I'm simply a technology enthusiast who love learn and share new stuff.

Read More