ASP Net JWT Authentication – Test

Assuming a basic ASP Net API is already existing and using JWT as method of authentication. ASP Net Rest API – Authenticate – JWT

To ensure authentication work as expected, let’s add unittests.

As application would normaly use a persistent database that would need whiping after each test, first thing is to drop used database and replaye it by a non persistent in memory database.

using System;
using System.Linq;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using tryout_blazor_api.Server;

namespace Tests.IntegrationTests
{
    public class TestingWebAppFactory<TEntryPoint> : WebApplicationFactory<Program> where TEntryPoint : Program
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));

                services.Remove(descriptor);

                services.AddDbContext<ApplicationDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                });
            });
        }
    }
}

Wrapping some recurring actions in a base class.

using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using tryout_blazor_api.Shared;
using tryout_blazor_api.Shared.Auth;
using Xunit;

namespace Tests.IntegrationTests
{
    public class AuthTestBase : IAsyncLifetime
    {
        protected readonly TestingWebAppFactory<Program> _factory;
        private static bool Initialized = false;

        public AuthTestBase()
        {
            _factory = new TestingWebAppFactory<Program>();
        }

        public async Task InitializeAsync()
        {
            if(Initialized)
                return;

            using (var client = _factory.CreateClient())
            {
                await RegisterAsUser(client);
            }

            Initialized = true;
        }

        public Task DisposeAsync()
        {
            return Task.CompletedTask;
        }


        protected async Task<string> LoginAsUser(HttpClient client)
        {
            return await LoginAs(client, "user", "P4$$w0rd");
        }

        protected async Task<string> LoginAs(HttpClient client, string user, string pass)
        {
            LoginModel loginData = new () {
                Username = user,
                Password = pass
            };

            var responseLogin = await client.PostAsJsonAsync("Auth/login", loginData);
            responseLogin.EnsureSuccessStatusCode();
            var responseString = await responseLogin.Content.ReadAsStringAsync();
            var resultLogin = JsonSerializer.Deserialize<LoginToken>(responseString, new JsonSerializerOptions()
            {
                PropertyNameCaseInsensitive = true
            });
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", resultLogin!.Token);

            return resultLogin!.Token;
        }

        protected async Task<Response?> RegisterAsUser(HttpClient client)
        {
            return await RegisterAs(client, "user", "P4$$w0rd", "user@example.net");
        }

        protected async Task<Response?> RegisterAs(HttpClient client, string user, string pass, string mail)
        {
            RegisterModel regData = new () {
                Username = user,
                Password = pass,
                Email = mail
            };

            var response = await client.PostAsJsonAsync("Auth/register", regData);
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            var resultRegister = JsonSerializer.Deserialize<Response>(responseString, new JsonSerializerOptions()
            {
                PropertyNameCaseInsensitive = true
            });
            return resultRegister;
        }
    }
}

As previously the /WeatherForcast API was configured to require authentication, we now can add tests if we’re correctly denied if not logged in.
As well as creating a new account, logging in with it’s data and being grated access to /WeatherForcast.

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

namespace Tests.IntegrationTests
{
    public class AuthTests : AuthTestBase
    {
        [Fact]
        public async Task Unauthorized_Access_is_prevented()
        {
            // Arrange
            var client = _factory.CreateClient();
            
            // Act
            // Assert
            await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
            {
                var responseAuthorized = await client.GetAsync("WeatherForecast");
                responseAuthorized.EnsureSuccessStatusCode();
            });
        }

        [Fact]
        public async Task Authorized_Access_is_permitted()
        {
            // Arrange
            var client = _factory.CreateClient();

            // Act
            string bearerToken = await LoginAsUser(client);
            var responseAuthorized = await client.GetAsync("WeatherForecast");
            
            // Assert
            responseAuthorized.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task Register()
        {
            // Arrange
            var client = _factory.CreateClient();

            // Act
            var resultReg = await RegisterAs(client, "User2", "P4$$w0rd", "User2@example.net");
            var resultLogin = await LoginAs(client, "User2", "P4$$w0rd");

            // Assert
            Assert.NotNull(resultReg);
            Assert.Contains("Success", resultReg!.Status);
            Assert.NotEqual("", resultLogin);
        }

        [Fact]
        public async Task RegisterShortPassword()
        {
            // Arrange
            var client = _factory.CreateClient();

            // Act
            var exception = await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
            {
                var resultReg = await RegisterAs(client, "User3", "pass", "User3@example.net");
            });

            // Assert
            Assert.NotEqual("", exception.Message);
        }

        [Fact]
        public async Task Token_duration()
        {
            // Arrange
            var client = _factory.CreateClient();
            var expiryMin = DateTime.Now + TimeSpan.FromHours(1);
            var expiryMax = DateTime.Now + TimeSpan.FromHours(6);

            // Act
            string bearerToken = await LoginAsUser(client);
            var handler = new JwtSecurityTokenHandler();
            var token = handler.ReadToken(bearerToken) as JwtSecurityToken;

            // Assert
            Assert.NotEqual("", bearerToken);
            Assert.NotNull(token);
            Assert.Equal(1, token!.ValidTo.CompareTo(expiryMin));
            Assert.Equal(-1, token!.ValidTo.CompareTo(expiryMax));
        }
    }

To comply with DRY principle the Register Test should definetly be broken down further and probably be split into helper methods. As logging in and registering will most probably be used several times but then not as the main test target.

Full code can be found here: https://github.com/sukapx/tryout_blazor_api/tree/3cc988daa81bdc59cf18055a45d4a6fc5e1b5391

Leave a Reply

Your email address will not be published. Required fields are marked *