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); } }