Menu

Unit testing Basic Authentication Handler in asp.net core (C# )

Recently I have started to concentrate on unit testing more, I started adding unit tests to existing code. Code base was having 5-10% unit tests, so I stated adding more. Lets see how you can subject/ candidate who requires unit tests.

    public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        private readonly IUserService _userService;

        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IUserService userService)
            : base(options, logger, encoder, clock)
        {
            _userService = userService;
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            // skip authentication if endpoint has [AllowAnonymous] attribute
            var endpoint = Context.GetEndpoint();
            if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
                return AuthenticateResult.NoResult();

            if (!Request.Headers.ContainsKey("Authorization"))
                return AuthenticateResult.Fail("Missing Authorization Header");

            LaptopAgentUser user = null;
            try
            {
                var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
                var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
                var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
                var username = credentials[0];
                var password = credentials[1];
                user = await _userService.Authenticate(username, password);
            }
            catch
            {
                return AuthenticateResult.Fail("Invalid Authorization Header");
            }

            if (user == null)
                return AuthenticateResult.Fail("Invalid Username or Password");

            var claims = new List<Claim> {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.UserName),
            };

            claims.AddRange(user.Roles.Select(role => new Claim(ClaimTypes.Role, role)).ToArray());

            var identity = new ClaimsIdentity(claims, Scheme.Name);
            var principal = new ClaimsPrincipal(identity);
            var ticket = new AuthenticationTicket(principal, Scheme.Name);

            return AuthenticateResult.Success(ticket);
        }
    }

Lets see how you can unit test the above handler, I will be using the Xunit (my preferred) and Moq, I personally prefer NSubstitute, but the project was already using Moq and I didn’t want to change it.

you can Moq (Mock) everything at the beginning or you can do it per test case

    public class BasicAuthenticationHandlerTests
    {
        private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _options;
        private readonly Mock<ILoggerFactory> _logger;
        private readonly Mock<UrlEncoder> _encoder;
        private readonly Mock<ISystemClock> _clock;
        private readonly Mock<IUserService> _userService;

        public BasicAuthenticationHandlerTests()
        {
            _options = new Mock<IOptionsMonitor<AuthenticationSchemeOptions>>();
     
            _options
                .Setup(x => x.Get(It.IsAny<string>()))
                .Returns(new AuthenticationSchemeOptions());

            var logger = new Mock<ILogger<BasicAuthenticationHandler>>();
            _logger = new Mock<ILoggerFactory>();
            _logger
                .Setup(x => x.CreateLogger(It.IsAny<String>()))
                .Returns(logger.Object);

            _encoder = new Mock<UrlEncoder>();
            _clock = new Mock<ISystemClock>();
            _userService = new Mock<IUserService>();
        }

        [Fact(Display="Authorization header not provided should return invalid authorization header")]
        public async Task AuthorizationHeaderNotProvidedShouldReturnInvalidAuthorizationHeader()
        {
            var context = new DefaultHttpContext();
            var _handler = new BasicAuthenticationHandler(_options.Object, _logger.Object, _encoder.Object, _clock.Object, _userService.Object);

            await _handler.InitializeAsync(new AuthenticationScheme(AuthenticationSchemes.BasicAuth, null, typeof(BasicAuthenticationHandler)), context);
            var result = await _handler.AuthenticateAsync();

            Assert.False(result.Succeeded);
            Assert.Equal("Missing Authorization Header", result.Failure.Message);
        }

        [Fact(Display="Authorization header is empty should return invalid authorization header")]
        public async Task AuthorizationHeaderEmptyShouldReturnInvalidAuthorizationHeader()
        {
            var context = new DefaultHttpContext();
            var authorizationHeader = new StringValues(String.Empty);
            context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader);

            var _handler = new BasicAuthenticationHandler(_options.Object, _logger.Object, _encoder.Object, _clock.Object, _userService.Object);

            await _handler.InitializeAsync(new AuthenticationScheme(AuthenticationSchemes.BasicAuth, null, typeof(BasicAuthenticationHandler)), context);
            var result = await _handler.AuthenticateAsync();

            Assert.False(result.Succeeded);
            Assert.Equal("Invalid Authorization Header", result.Failure.Message);
        }

        [Fact(Display="Authorization header is invalid should return invalid authorization header")]
        public async Task AuthorizationHeaderInvalidShouldReturnInvalidAuthorizationHeader()
        {
            var context = new DefaultHttpContext();
            var authorizationHeader = new StringValues("aaaaaa");
            context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader);

            var _handler = new BasicAuthenticationHandler(_options.Object, _logger.Object, _encoder.Object, _clock.Object, _userService.Object);

            await _handler.InitializeAsync(new AuthenticationScheme(AuthenticationSchemes.BasicAuth, null, typeof(BasicAuthenticationHandler)), context);
            var result = await _handler.AuthenticateAsync();

            Assert.False(result.Succeeded);
            Assert.Equal("Invalid Authorization Header", result.Failure.Message);
        }

        [Fact(Display="Invalid credentials should return fail")]
        public async Task InvalidCredentialsShouldReturnFail()
        {
            var context = new DefaultHttpContext();
            var authorizationHeader = new StringValues("Basic dXNlcm5hbWVAY2JhLmNvbS5hdTpQYXNzd29yZEAhMjIyMg==");
            context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader);

            _userService
                .Setup(x => x.Authenticate("username@org.com", "Password@!XYZ"))
                .ReturnsAsync(null as User);

            var _handler = new BasicAuthenticationHandler(_options.Object, _logger.Object, _encoder.Object, _clock.Object, _userService.Object);

            await _handler.InitializeAsync(new AuthenticationScheme(AuthenticationSchemes.BasicAuth, null, typeof(BasicAuthenticationHandler)), context);
            var result = await _handler.AuthenticateAsync();

            Assert.False(result.Succeeded);
            Assert.Equal("Invalid Username or Password", result.Failure.Message);
        }

        [Fact(Display="Valid credentials should return success")]
        public async Task ValidCredentialsShouldReturnSuccess()
        {
            var context = new DefaultHttpContext();
            var authorizationHeader = new StringValues("Basic <Base64Encoded Username:Password>");
            context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader);

            _userService
                .Setup(x => x.Authenticate("username@org.com", "Password@!XYZ"))
                .ReturnsAsync( new User { Id ="1",  UserName = "username@org.com", Roles = new List<string>{ "Admin" } });

            var _handler = new BasicAuthenticationHandler(_options.Object, _logger.Object, _encoder.Object, _clock.Object, _userService.Object);

            await _handler.InitializeAsync(new AuthenticationScheme(AuthenticationSchemes.BasicAuth, null, typeof(BasicAuthenticationHandler)), context);
            var result = await _handler.AuthenticateAsync();

            Assert.True(result.Succeeded);
            Assert.Equal(3, result.Principal.Claims.Count());
            Assert.Equal("1", result.Principal.Claims.First().Value);
            Assert.Equal("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", result.Principal.Claims.First().Type);
            Assert.Equal("username@cba.com.au", result.Principal.Claims.Skip(1).First().Value);
            Assert.Equal("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", result.Principal.Claims.Skip(1).First().Type);
            Assert.Equal("Laptop Agent", result.Principal.Claims.Last().Value);
            Assert.Equal("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", result.Principal.Claims.Last().Type);
        }

    }

 

Leave a comment