Integration testing for dotnet core APIs: Handling APIs behind authentication

Article Banner
Author(s): Ajay Kumar
Published On: 22 Feb 2025

Integration testing for APIs behind authentication


This is continuation of the Integration testing in dotnet series.

This time we will be covering on how to test APIs which are behind authentication. We will be extending the same code repo we used in the previous article on handling 3rd party services (folder name DemoWith3rdPartyService).

Please make sure to checkout the previous articles in the series to get the most out of this article:

Integration testing for dotnet core APIs: Introduction

This is going to be a multi-part series on integration tests from introduction to advanced use cases.

devcodex.in
Blog Image
Integration testing for dotnet core APIs: Handling database

Welcome to the 2nd post in our Integration testing series. You may check out the previous post that introduces integration testing.

devcodex.in
Blog Image
Integration testing for dotnet core APIs: Handling 3rd party service calls using wiremock

This is continuation of the Integration testing in dotnet series. This time we will be covering the scenario where APIs interact with 3rd party services.

devcodex.in
Blog Image

Let’s get started


To make this happen we have created an authenticated version of our SuperHero API. The API is called /SuperHero/private. Refer the swagger screenshot below.

Swagger API
Figure 1 : Swagger API

Our API action method in code will look like below. Notice the [Authorize] attribute with role required as Admin


            [HttpGet("private")]
            [Authorize(Roles = "Admin")]
            public async Task<IEnumerable<SuperHero>> GetPrivate()
            {
                return await superHeroRepository.GetAllSuperHeroes();
            }
        
Code Sample #2 : Authenticated API action method

We have done some wiring to setup JWT based authentication scheme in our SuperHero API project. However, please note that, with little tweaks in the test setup, this should work for all kinds of authentication schemes.

For a quick reference, here is the authentication setup. The auth settings are being read from appsettings.


            builder.Services.AddAuthentication(options =>
            {
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
            {
                IdentityModelEventSource.ShowPII = true;
                options.SaveToken = true;
                options.IncludeErrorDetails = true;
                var authSettings = builder.Configuration.GetSection("JwtBearerSettings")
                    .Get<JwtBearerSettings>();

                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidAudience = authSettings!.Audience,
                    ValidIssuer = authSettings.Issuer,
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings.SigningKey))
                };
            });
        
Code Sample #3 : Authentication setup

After all that setup, when we try to access this API without token then we get 401 error as expected

Swagger API response
Figure 2 : Swagger API response

Now that we understand the authentication setup, we can quickly head to https://jwt.io to generate a token by specifying the claims and signing key. Notice we have also setup the role as admin in the token. This is just to verify that the API functions correctly. We will cover how can we generate this token for integration tests later.

Access token jwt
Figure 3 : Access token jwt

Now once we get hold of the token, we can set the token in swagger.

Setting auth token
Figure 4 : Setting auth token

Once the token is configured, we should expect 200 success with response data like below.

Getting response under authentication
Figure 5 : Getting response under authentication

Now that we have seen our authenticated API working, we can proceed to write the integration tests for it.

The setup


Now this is going to be a bit complex setup, however, it will help us in testing different and complex scenarios. The following steps are needed before we begin writing tests.

Step 1: A class to help us create authentication claims

Why is this needed?

We need this to create HTTP client with different set of authentication claims for different kinds of auth flows. For example, an API might work only for Admin role and another may work for some other role having fewer privileges. Our AuthClaimsProvider looks like below:


                public class AuthClaimsProvider
                {
                    public IList<Claim> Claims { get; } = new List<Claim>();

                    public static AuthClaimsProvider WithAdminClaim()
                    {
                        var provider = new AuthClaimsProvider();
                        provider.Claims.Add(new Claim(ClaimTypes.Role, "Admin"));

                        return provider;
                    }
                    
                    public static AuthClaimsProvider WithAnonymousClaim()
                    {
                        var provider = new AuthClaimsProvider();
                        provider.Claims.Add(new Claim(ClaimTypes.Role, "Anonymous"));

                        return provider;
                    }
                }
            
Code Sample #4 : AuthClaimsProvider class

Step 2: An extension method to create HTTP client with required auth claims

Why is this needed?

It's needed because when we create a default HTTP client for tests using CustomApiFactory, the created HTTP client instance does not hold any auth token. However, for our authenticated APIs, we want our HTTP client to have the required token.


                public static HttpClient CreateClientWithClaim(this CustomApiFactory factory, AuthClaimsProvider claims)
                {
                    var client = factory.WithWebHostBuilder(builder =>
                        {
                            builder.ConfigureTestServices(services =>
                            {
                                // Removing existing instance of AuthClaimsProvider to register with instance received as param
                                var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(AuthClaimsProvider));

                                if (descriptor != null)
                                {
                                    services.Remove(descriptor);
                                    services.AddScoped(_ => claims);
                                }
                            });
                        })
                        .CreateClient();

                    return client;
                }
            
Code Sample #5 : Extension method for HTTP client

Step 3: Setting up a service to introduce the authentication ticket/token in the request

Why is this needed?

It's needed because in real scenarios, users will get tokens by performing any auth process, like:

  • By providing username and password
  • By using third-party auth services like Google, Facebook, LinkedIn, etc.
  • By using ClientId and Secret to obtain a token via client credentials flow

Since our integration tests should not require any user intervention, we will be introducing a TestAuthHandler so that whenever we try to create an HTTP client to call our service, we should get a client with the authentication token as per our test setup.


                public class TestAuthHandler(
                    IOptionsMonitor<AuthenticationSchemeOptions> options,
                    ILoggerFactory logger,
                    UrlEncoder encoder,
                    AuthClaimsProvider claimsProvider)
                    : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
                {
                    public const string SchemeName = "Test";

                    // These claims will be set by the extension method `CreateClientWithClaim` used to create HTTP client in tests
                    private readonly IList<Claim> _claims = claimsProvider.Claims;

                    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
                    {
                        // Sometimes we do not want to have our HTTP client to hold a token.
                        // This line will make sure the default HTTP client created from CustomApiFactory is not with auth ticket
                        if (_claims == null || !_claims.Any())
                        {
                            return Task.FromResult(AuthenticateResult.NoResult());
                        }
                        
                        var identity = new ClaimsIdentity(_claims, SchemeName);
                        var principal = new ClaimsPrincipal(identity);
                        var ticket = new AuthenticationTicket(principal, SchemeName);

                        return Task.FromResult(AuthenticateResult.Success(ticket));
                    }
                }
            
Code Sample #6 : TestAuthHandler class

Step 4: Registering the TestAuthHandler service

Why is this needed?

It's needed so that whenever we try to get an HTTP client from the CustomApiFactory, we get an HTTP client with an auth token in it (unless we do not want it by using empty claims).


                builder.ConfigureTestServices(services =>
                {
                    services.AddAuthentication(TestAuthHandler.SchemeName)
                        .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.SchemeName, _ => { });

                    services.AddScoped(_ => new AuthClaimsProvider());
                });
            
Code Sample #7 : Registering TestAuthHandler

Finally, the tests


Let's add a test to our SuperHeroApiTests. Please note that the below test is with the default HTTP client, which does not hold any successful auth ticket:


            [Fact(DisplayName = "Get all superheroes authenticated API returns all superheroes")]
            public async Task Get_All_SuperHeroes_Authenticated_Returns_List_Of_SuperHero()
            {
                // Arrange
                factory.SharedFixture.SuperHeroDbContext.SuperHero.AddRange(new List<SuperHeroApiWith3rdPartyService.Data.Models.SuperHero>()
                {
                    new(11, "Batman","Bruce Wayne","Short distance fly,Common sense","Gotham", 40),
                    new(22, "Superman", "Clark kent", "Fly, Shoot laser beam, Super strength, ice breath","Gotham", 42),
                    new(33, "Robin", "John Blake", "Detective","Gotham", 35)
                });
                await factory.SharedFixture.SuperHeroDbContext.SaveChangesAsync();
                
                // Act
                using var httpClient = factory.CreateClient(); // Default HTTP client
                var response = await httpClient.GetAsync("/SuperHero/private");
                
                // Assert
                response.StatusCode.Should().Be(HttpStatusCode.OK);
                var superHeroes = await response.Content.ReadFromJsonAsync>List>SuperHeroApiWith3rdPartyService.Data.Models.SuperHero>>();
                superHeroes.Should().NotBeEmpty();
                superHeroes.Should().Contain(s => s.SuperName == "Batman");
            }
        
Code Sample #8 : Test with default HTTP client

Upon running this, we get a result like below: 401, since the auth token is not there.

Failing test when no token is provided
Figure 6 : Failing test when no token is provided

Let's change the code to create the HTTP client with some claims this time:


            // Replace
            using var httpClient = factory.CreateClient();

            // With
            using var httpClient = factory.CreateClientWithClaim(AuthClaimsProvider.WithAnonymousClaim());
        
Code Sample #9 : Test with anonymous claims

Now our API is using the token. Let's run the tests.

Failing test with anonymous role
Figure 7 : Failing test with anonymous role

The test run above also failed with a 403 status code. Our authentication issue is resolved since we no longer receive a 401 error. However, we still get a 403 because the token contains the Anonymous role, while our API requires the Admin role, resulting in a forbidden error.

Let's fix our test by assigning the correct token with the Admin role when creating the HTTP client:


            // Replace
            using var httpClient = factory.CreateClientWithClaim(AuthClaimsProvider.WithAnonymousClaim());

            // With
            using var httpClient = factory.CreateClientWithClaim(AuthClaimsProvider.WithAdminClaim());
        
Code Sample #10 : Test with admin claims

Now, if we run the test after the above changes, our test succeeds with all assertions.

Tests passing when correct claims are provided
Figure 8 : Tests passing when correct claims are provided

Thats about it for this article. Hope you liked it.

For reference, the code repository being discussed is available at github: https://github.com/ajaysskumar/SuperHeroSolution

Thanks for reading through. Please share feedback, if any, in comments or on my email ajay.a338@gmail.com

Copyright © 2025 Dev Codex

An unhandled error has occurred. Reload 🗙