When writing integration tests it can be a challenge when dealing with API endpoints that has authentication enabled.

This often leads to huge mocking efforts or running the integration tests using actual authentication providers which leads to having to manage secrets.

Additionally, the tests then cannot run in isolation due to external dependencies.

The end result of this can often be that developers avoid the pain by coming up with creative ways to not test the authorization in integration tests.

One of the benefits of the AuthN/AuthZ middleware in AspNet Core is that it can be configured depending on the environment it runs in which we will utilize in this post.

This isn't something incredibly groundbreaking and is inspired by the user-jwts feature of .NET (docs here).

TL;DR

The quick and dirty approach to this is to override the middleware configuration to use a symmetric key for token validation and provide helpers for generating tokens with the claims we want.

  • Create a utility class that generates a random signing key and declares an issuer
  • Create a utility class that uses this information to build and sign a JWT token using JwtSecurityTokenHandler
  • Create a base class for the integration tests that configures JwtBearerOptions to validate tokens using the previously generated signing key and issuer

Sample code here: https://github.com/FrodeHus/testing-jwt-apps

Lets go!

To achieve our goal, we'll need to provide the AspNet Core middleware with a configuration that it will use to validate our JWT tokens. To make this simple, we'll use a symmetric key which enables us to avoid a complex infrastructure.

Of course, symmetric keys are not ideal in production, but we're doing integration tests here and not testing the authentication provider itself.

Creating our provider

We'll start off by creating a new static class JwtTokenProvider which will look like this:

// our fake issuer - can be anything
public static string Issuer { get; } = "Sample_Auth_Server";

// Our random signing key - used to sign and validate the tokens
public static SecurityKey SecurityKey { get; } = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray());

// the signing credentials used by the token handler to sign tokens
public static SigningCredentials SigningCredentials { get; } = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);

// the token handler we'll use to actually issue tokens
public static readonly JwtSecurityTokenHandler JwtSecurityTokenHandler = new();

That's it! Now we have a way of issuing proper tokens which will validate successfully.

Configuring the middleware

The authentication middleware will not magically make use of our fantastic provider, so we'll have to be responsible adults and inform it about its existence.

We'll create a base class that all integration tests will inherit from - EndToEndTestCase.

To tell the middleware how to validate tokens, we'll need to add a configuration of JwtBearerOptionsto dependency injection.

The key points we'll need is:

  • The issuer that issued the token
  • The signing key that must be used to validate the token

Strangely enough, these are exactly the things we have defined in our custom provider above!

Our configuration then simply becomes:

services.Configure<JwtBearerOptions>(
	JwtBearerDefaults.AuthenticationScheme,
		options =>
		{
			options.Configuration = new OpenIdConnectConfiguration
			{
				Issuer = JwtTokenProvider.Issuer,
			};
            // ValidIssuer and ValidAudience is not required, but it helps to define them as otherwise they can be overriden by for example the `user-jwts` tool which will cause the validation to fail
			options.TokenValidationParameters.ValidIssuer = JwtTokenProvider.Issuer;
			options.TokenValidationParameters.ValidAudience = JwtTokenProvider.Issuer;
			options.Configuration.SigningKeys.Add(JwtTokenProvider.SecurityKey);
		}
);

Entire class can be seen here: testing-jwt-apps/EndToEndTestCase.cs at main · FrodeHus/testing-jwt-apps · GitHub

Writing our first test

To write our first test, we'll just have to inherit from EndToEndTestCase, issue our token and use the endpoint as normal.

[Fact]
public async Task Should_Allow_Operators_To_Retrieve_Secrets()
  {
    var token = JwtTokenProvider.JwtSecurityTokenHandler.WriteToken(
        new JwtSecurityToken(
            JwtTokenProvider.Issuer,
            JwtTokenProvider.Issuer,
            new List<Claim> { new(ClaimTypes.Role, "Operator"), },
            expires: DateTime.Now.AddMinutes(30),
            signingCredentials: JwtTokenProvider.SigningCredentials
        )
    );

	Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

    var response = await Client.GetAsync("/admin");
      response.StatusCode.Should().Be(HttpStatusCode.OK);
}

Now, the creation of the token is very verbose and as to not clutter up our test too much, we can make a helper class for that.

Making creation of JWT tokens a bit easier

We'll start off with a new class TestJwtToken which will be a fluent interface (because reasons - you do you).

This will simply contain the configuration of our token such as the claims it will have, expiration and anything you think you'll need.

public class TestJwtToken
{
    public List<Claim> Claims { get; } = new();
    public int ExpiresInMinutes { get; set; } = 30;
}

We'll then add a convenient way of adding claims such as role:

public TestJwtToken WithRole(string roleName)
{
  Claims.Add(new Claim(ClaimTypes.Role, roleName));
  return this;
}

Add other methods as you see fit - perhaps one for a plain old AddCustomRole(..)?

Then we'll need a method to assemble our token:

public string Build()
{
    var token = new JwtSecurityToken(
        JwtTokenProvider.Issuer,
        JwtTokenProvider.Issuer,
        Claims,
        expires: DateTime.Now.AddMinutes(ExpiresInMinutes),
        signingCredentials: JwtTokenProvider.SigningCredentials
    );
    return JwtTokenProvider.JwtSecurityTokenHandler.WriteToken(token);
}

The end result is that we can now issue our token simply by doing:

var token = new TestJwtToken().WithRole("Admin").Build();

Once we make use of this in our tests, they start to look much simpler:

[Fact]
public async Task Should_Allow_Admins()
{
    var token = new TestJwtToken().WithRole("Admin").WithUserName("testuser").Build();
    
    Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

    var response = await Client.GetAsync("/admin");
    response.StatusCode.Should().Be(HttpStatusCode.OK);
}

Conclusion

Now we can write our integration tests without external dependencies and still utilize our production-ready code with authorization policies, roles and whatnot.

Hopefully this was helpful and makes it easier to include authorization in tests 🤗

The full code to this post can be found here: FrodeHus/testing-jwt-apps (github.com)