Skip to main content
Jesper Karlshøj

ASP.NET Core integration test and Authentication

Testing authentication in web application can be sometimes be difficuelt. Testing with different users, different roles or other claims. Testing with invalid or expired claims.

In this blog post I will go though how to easily manipulate the token generation in a ASP.NET core starter project.

Setup Project #

First we setup a new dotnet API project dotnet new webapi -f net8.0 -controllers

Next we setup authentication. See this Microsoft documentation on Consume security Tokens When invoked this will expect a bearer token from the authority on https://localhost:8083 and have a aud claim with the value orders. Because no signing keys are specified the Microsoft authentication library will try to download they signing keys from the well-known endpoint.

builder.Services.AddAuthentication(options =>
{
  options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
  options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

}).AddJwtBearer(options =>
{
  options.Authority = "https://localhost:8083";
  options.RequireHttpsMetadata = false;
  options.Audience = "orders";
});

...

app.UseAuthentication();
app.UseAuthorization();

Lastly add [Authorize] to the WeatherForeCastController.

ASP.NET Core TestServer #

New Test project #

I use Xunit, but this can easily be changed to NUnit or another test framework.

mkdir DemoAuth.Tests
cd DemoAuth.Tests
dotnet new xunit
cd ..
dotnet sln add .\DemoAuth.Tests\DemoAuth.Tests.csproj

Before adding a new test case we must prepare Program.cs.

app.Run();

+ public partial class Program { }

Create a new test case. This is based on ASP.NET Core Integration Tests. Reference DemoAuth project from DemoAuth.Tests project.

Add BasicTests.cs to DemoAuth.Tests project.

namespace DemoAuth.Tests;

public class BasicTests : IClassFixture<WebApplicationFactory<Program>>
{

  private readonly WebApplicationFactory<Program> _factory;

  public BasicTests(WebApplicationFactory<Program> factory)
  {
    _factory = factory;
  }

    [Fact]
    public async Task AuthTest()
    {

    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.GetAsync("WeatherForecast");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);

  }
}

This will fail with unauthenticated, which is inteded at this stage.

Mock Authentication #

We need to create a custom WebApplicationFactory to override the authentication configuration.

public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{

  public X509Certificate2 Certificate { get; init; } = CertUtil.BuildCertificate();

  protected override void ConfigureWebHost(IWebHostBuilder builder)
  {
    base.ConfigureWebHost(builder);

    builder.ConfigureTestServices(services =>
    {
      services.PostConfigureAll<JwtBearerOptions>(opts =>
      {
        opts.TokenValidationParameters = new TokenValidationParameters
        {
          IssuerSigningKeys = new[]
          {
              new RsaSecurityKey(Certificate.PublicKey.GetRSAPublicKey())
          },
          ValidIssuer = "https://component-test",
          ValidAudience = "https://component-under-test",
          NameClaimType = "sub",
          RoleClaimType = "roles",
        };


        opts.Audience = "https://component-under-test";
        opts.Authority = "https://component-test";

      });
    });
  }

A few important configurations here:

  1. We set the public key from the certificate to IssuerSigningKeys. Must production configurations will download the IssuerSigningKeys from the IdP, via a well-known URI.
  2. We override the issuer and audience. It will not be an issue to keep this similar to your normal configued values.

Generating self signed certificates for the test case, instead of having a certificate file in source. This will ensure that the certificate will not expire and will not generate alerts for different scanning tools for having certificates and secrets.

internal static class CertUtil
{
  private static readonly string certificatename = "WebDummy";

  internal static X509Certificate2 BuildCertificate()
  {
    SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder();
    sanBuilder.AddIpAddress(IPAddress.Loopback);
    sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);
    sanBuilder.AddDnsName("localhost");
    sanBuilder.AddDnsName(Environment.MachineName);

    X500DistinguishedName distinguishedName = new X500DistinguishedName($"CN={certificatename}");

    using RSA rsa = RSA.Create(2048);

    var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

    request.CertificateExtensions.Add(
      new X509KeyUsageExtension(
        X509KeyUsageFlags.DataEncipherment | 
        X509KeyUsageFlags.KeyEncipherment | 
        X509KeyUsageFlags.DigitalSignature, 
        false));

    request.CertificateExtensions.Add(
      new X509EnhancedKeyUsageExtension(
        new OidCollection
        {
          new Oid("1.3.6.1.5.5.7.3.1")
        },
        false ));

    request.CertificateExtensions.Add(sanBuilder.Build());

    var certificate = request.CreateSelfSigned(new DateTimeOffset(DateTime.UtcNow.AddDays(-1)), new DateTimeOffset(DateTime.UtcNow.AddDays(1)));


    return certificate;
  }
}

Create Mock Bearer token #

Creating customized tokens in C# can be done with JwtSecurityTokenHandler. As we have generated a self-signed certificate and added the public key to our API, we can issue valid tokens with the private key.

public string CreateToken()
  {
    var signingCredentials = new SigningCredentials(new X509SecurityKey(Certificate), SecurityAlgorithms.RsaSha256);

    var notBefore = DateTime.UtcNow.AddHours(-2);
    var expires = notBefore.Add(TimeSpan.FromHours(3));

    List<Claim> claims = new()
    {
      new Claim("sub", "[email protected]"),
      new Claim("roles", "admin")
    };


    var identity = new ClaimsIdentity(claims);

    var securityTokenDescriptor = new SecurityTokenDescriptor
    {
      Audience = "https://component-under-test",
      Issuer = "https://component-test",
      NotBefore = notBefore,
      Expires = expires,
      SigningCredentials = signingCredentials,
      Subject = identity
    };

    JwtSecurityTokenHandler _securityTokenHandler = new();
    var token = _securityTokenHandler.CreateToken(securityTokenDescriptor);

    var encodedAccessToken = _securityTokenHandler.WriteToken(token);

    return encodedAccessToken;
  }
}

Update your test class

public class BasicTests : IClassFixture<CustomWebApplicationFactory<Program>>
{

  private readonly CustomWebApplicationFactory<Program> _factory;

  public BasicTests(CustomWebApplicationFactory<Program> factory)
  {
    _factory = factory;
  }

  [Fact]
  public async Task NoAuthTest()
  {

    // Arrange
    var client = _factory.CreateClient();

    var request = new HttpRequestMessage(HttpMethod.Get, "WeatherForecast");

    // Act
    var response = await client.SendAsync(request);

    // Assert
    Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);

  }

  [Fact]
  public async Task AuthTest()
  {

    // Arrange
    var client = _factory.CreateClient();

    var request = new HttpRequestMessage(HttpMethod.Get, "WeatherForecast");
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _factory.CreateToken());

    // Act
    var response = await client.SendAsync(request);

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);

  }
}

Conclusion #

This setup will override your default configuration with using a selfsigned certificate. This will not replace integrations tests with correct configurations with your Identity Provider (IdP). It will however give you an easy way to test your authorization code without having different the hazzle to create and maintain different users in your IdP where Password/Secrets will need to be rotated and kept away from source code.