Search code examples
asp.net-coreidentityserver4kestrel-http-server

Integration Tests for WebApi that is protected using OATH through a separate microservice that hosts IdentityServer


Background

We have an ASP.NET Core 2.1 solution with microservices that contain WebApi methods. We use Identity Server 4 for authentication. We have these services, each in a separate project.

  1. crm. Contains various WebAPI methods.
  2. fraud. Contains various WebAPI methods.
  3. notifications. Contains various WebAPI methods.
  4. authentication. Contains the IdentityServer implementation.

The authentication service doesn't have a controller or WebAPI methods, it's just where IdentityServer4 is configured and implemented. The resources,clients, scopes etc are defined here and in the Startup it also has the initialisation:

        // Identity Server
        services.AddIdentityServer()
            .AddSigningCredential(Configuration.GetValue<string>("Certificates:TokenCertificate"), System.Security.Cryptography.X509Certificates.StoreLocation.LocalMachine, NameType.Thumbprint)
            .AddInMemoryApiResources(Config.GetApiResources())
            .AddInMemoryClients(Config.GetClients(Configuration))
            .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
            .AddProfileService<ProfileService>();

Our authentication works fine when we test with Postman, we have to first connect to the token endpoint of the Authentication service and request a Bearer token. We can then pass it in the Authorization header of the request and it works ok.

It's now time to update our integration tests, because they all fail since we implemented Authentication, they return status Unauthorized which is to be expected.

Question

How do we go about calling the authentication service from our integration tests, since it's a separate microservice?

We have followed standard practices for creating integration tests for our microservices. We have one test project for each microservice, and it has one fixture class creates a webHostBuilder using the Startup of that microservice, and a testServer HttpClient that will be queried by the integration tests.

        var webHostBuilder = new WebHostBuilder()
               .UseEnvironment("Testing")
               .UseStartup<Startup>()
                .ConfigureTestServices(s =>
                {
                    addServices?.Invoke(s);
                })
               .ConfigureAppConfiguration((builderContext, config) =>
               {
                   Configuration = config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true).AddEnvironmentVariables().Build();
                   this.SignatureCertificate = CertificateHelper.FindCertificateByThumbprint(Configuration.GetValue<string>("Certificates:SignatureThumbprint"), StoreLocation.LocalMachine, StoreName.My);
                   this.EncryptionCertificate = CertificateHelper.FindCertificateByThumbprint(Configuration.GetValue<string>("Certificates:EncryptionThumbprint"), StoreLocation.LocalMachine, StoreName.My);
                   this.DecryptionCertificate = CertificateHelper.FindCertificateByThumbprint(Configuration.GetValue<string>("Certificates:DecryptionThumbprint"), StoreLocation.LocalMachine, StoreName.My);
                   this.ReadSignedCertificate = CertificateHelper.FindCertificateByThumbprint(Configuration.GetValue<string>("Certificates:ReadSignedThumbprint"), StoreLocation.LocalMachine, StoreName.My);
               });
        var testServer = new TestServer(webHostBuilder);

        this.Context = testServer.Host.Services.GetService(typeof(CrmContext)) as CrmContext;
        this.Client = testServer.CreateClient();

But now our integration tests must first request the token from the token endpoint. But the endpoint has not been launched by webHostBuilder because our ID4 integration is in a separate service.

Do we need to create a second TestServer from a second WebHostBuilder that uses the Startup of the Authentication ASP.NET Core service?

Any help is greatly appreciated.


Solution

  • We had to create a collection fixture class that launched a separate WebHostBuilder with our authentication service, and this fixture was made available to all integration tests by injecting it in the constructor.

    But that wasn't all, when we called the authenticated methods, the web API test server could not access the authentication test server. When using OATH to protect a web api, this web api must access this url

    http:///.well-known/openid-configuration

    The solution to this was to use the propery JwtBackChannelHandler of the IdentityServer options, in the configuration of IdentityServer. This is in the startup of our web api:

            //Authentication
            services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(options =>
                {
                    options.Authority = Configuration.GetValue<string>("Authentication:BaseUrl");
                    options.RequireHttpsMetadata = false;
                    options.ApiName = "Crm";
    
                    if (CurrentEnvironment.IsEnvironment("Testing"))
                    {
                        options.JwtBackChannelHandler = BackChannelHandler;
                    }
    
                });
    

    BackChannelHandler is a static property of our web api controller, and the authentication collection fixture mentioned above, could then use this static property to specify the handler that can be used to access the openid-configuration endpoint.

        public AuthenticationFixture()
        {
            //start the authentication service
            var authWebHostBuilder = new WebHostBuilder()
                    .UseEnvironment("Testing")
                    .UseStartup<Adv.Authentication.Api.Startup>()
                    .ConfigureTestServices(s =>
                    {
    
                        var userAccountWebServiceMock = new Mock<IUserAccountWebservice>();
                        userAccountWebServiceMock
                            .Setup(o => o.LogInAsync(It.IsAny<LogInCommand>()))
                            .Returns(Task.FromResult((ActionResult<LogInDto>)(new OkObjectResult(new LogInDto() { IsAuthenticated = true, UserId = 1 }))));
    
                        s.AddSingleton(userAccountWebServiceMock.Object);
    
                    })
                   .ConfigureAppConfiguration((builderContext, config) =>
                   {
                       Configuration = config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true).AddEnvironmentVariables().Build();
                   });
    
            var testServer = new TestServer(authWebHostBuilder);
    
            **Startup.BackChannelHandler = testServer.CreateHandler();**
            this.Client = testServer.CreateClient();
    
            GetAccessTokensAsync().Wait();
        }