Unit Test Custom AuthenticationHandler Middleware
P

1

33

How do you unit test custom middleware that inherits from AuthenticationHandler<AuthenticationSchemeOptions>?

My custom class that inherits from it is for Basic authentication.

    public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        private readonly IProvidePrincipal _principalProvider;

        public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IProvidePrincipal principalProvider)
            : base(options, logger, encoder, clock)
        {
            _principalProvider = principalProvider;
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (Request.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorizationHeader))
            {
                if (Credentials.TryParse(authorizationHeader, out Credentials credentials))
                {
                    var principal = await _principalProvider.GetClaimsPrincipalAsync(credentials.Username, credentials.Password, Scheme.Name);

                    if (principal != null)
                    {
                        var ticket = new AuthenticationTicket(principal, Scheme.Name);

                        return AuthenticateResult.Success(ticket);
                    }
                    else
                    {
                        return AuthenticateResult.Fail("Basic authentication failed.  Invalid username and password.");
                    }
                }
                else
                {
                    return AuthenticateResult.Fail("Basic authentication failed.  Unable to parse username and password.");
                }
            }

            return AuthenticateResult.Fail("Basic authentication failed.  Authorization header is missing.");
        }
    }
Prevaricate answered 20/11, 2019 at 20:47 Comment(0)
P
73

Unit testing custom middleware is relatively easy, but when you inherit from AuthenticationHandler, the base class throws a wrench in it. After looking everywhere and only finding integration tests, I was finally able to figure out how to do it.

Basic setup of the unit test that won't change per test.

    [TestClass]
    public class BasicAuthenticationTests
    {
        private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _options;
        private readonly Mock<ILoggerFactory> _loggerFactory;
        private readonly Mock<UrlEncoder> _encoder;
        private readonly Mock<ISystemClock> _clock;
        private readonly Mock<IProvidePrincipal> _principalProvider;
        private readonly BasicAuthenticationHandler _handler;

        public BasicAuthenticationTests()
        {
            _options = new Mock<IOptionsMonitor<AuthenticationSchemeOptions>>();
            
            // This Setup is required for .NET Core 3.1 onwards.
            _options
                .Setup(x => x.Get(It.IsAny<string>()))
                .Returns(new AuthenticationSchemeOptions());
            
            var logger = new Mock<ILogger<BasicAuthenticationHandler>>();
            _loggerFactory = new Mock<ILoggerFactory>();
            _loggerFactory.Setup(x => x.CreateLogger(It.IsAny<String>())).Returns(logger.Object);

            _encoder = new Mock<UrlEncoder>();
            _clock = new Mock<ISystemClock>();
            _principalProvider = new Mock<IProvidePrincipal>();

            _handler = new BasicAuthenticationHandler(_options.Object, _loggerFactory.Object, _encoder.Object, _clock.Object, _principalProvider.Object);
        }

Special note on _loggerFactory.Setup(x => x.CreateLogger(It.IsAny<String>())).Returns(logger.Object); If you do not do this, your unit tests will bomb after your handler finishes on a null reference in code that you cannot debug. It is because the base class calls CreateLogger in its constructor.

Now, you can setup the context using DefaultHttpContext to test the logic.

        [TestMethod]
        public async Task HandleAuthenticateAsync_NoAuthorizationHeader_ReturnsAuthenticateResultFail()
        {
            var context = new DefaultHttpContext();

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

            Assert.IsFalse(result.Succeeded);
            Assert.AreEqual("Basic authentication failed.  Authorization header is missing.", result.Failure.Message);
        }

Note that you cannot call HandleAuthenticateAsync directly as it is protected. The handler must be Initialized first then call AuthenticateAsync.

I included the rest of the logic to be tested below to give examples on how to manipulate the context and assert on the result for different testing scenarios.

        [TestMethod]
        public async Task HandleAuthenticateAsync_CredentialsTryParseFails_ReturnsAuthenticateResultFail()
        {
            var context = new DefaultHttpContext();
            var authorizationHeader = new StringValues(String.Empty);
            context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader);

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

            Assert.IsFalse(result.Succeeded);
            Assert.AreEqual("Basic authentication failed.  Unable to parse username and password.", result.Failure.Message);
        }

        [TestMethod]
        public async Task HandleAuthenticateAsync_PrincipalIsNull_ReturnsAuthenticateResultFail()
        {
            _principalProvider.Setup(m => m.GetClaimsPrincipalAsync(It.IsAny<String>(), It.IsAny<String>(), It.IsAny<String>())).ReturnsAsync((ClaimsPrincipal)null);

            var context = new DefaultHttpContext();
            var authorizationHeader = new StringValues("Basic VGVzdFVzZXJOYW1lOlRlc3RQYXNzd29yZA==");
            context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader);

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

            Assert.IsFalse(result.Succeeded);
            Assert.AreEqual("Basic authentication failed.  Invalid username and password.", result.Failure.Message);
        }

        [TestMethod]
        public async Task HandleAuthenticateAsync_PrincipalIsNull_ReturnsAuthenticateResultSuccessWithPrincipalInTicket()
        {
            var username = "TestUserName";
            var claims = new[] { new Claim(ClaimTypes.Name, username) };
            var identity = new ClaimsIdentity(claims, BasicAuthenticationHandler.SchemeName);
            var claimsPrincipal = new ClaimsPrincipal(identity);
            _principalProvider.Setup(m => m.GetClaimsPrincipalAsync(It.IsAny<String>(), It.IsAny<String>(), It.IsAny<String>())).ReturnsAsync(claimsPrincipal);

            var context = new DefaultHttpContext();
            var authorizationHeader = new StringValues("Basic VGVzdFVzZXJOYW1lOlRlc3RQYXNzd29yZA==");
            context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader);

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

            Assert.IsTrue(result.Succeeded);
            Assert.AreEqual(BasicAuthenticationHandler.SchemeName, result.Ticket.AuthenticationScheme);
            Assert.AreEqual(username, result.Ticket.Principal.Identity.Name);
        }
Prevaricate answered 20/11, 2019 at 20:47 Comment(4)
note that _options must return new AuthenticationSchemeOptions() when OptionsMonitor.Get(Scheme.Name) is called in InitializeAsync. When using NSubtitute, I had to explicitely say so (otherwise the test crashed)Tyree
@Tyree I had to do the same thing for NSubstitute for it not to crash. But I'm not sure I understand why given this line: github.com/aspnet/Security/blob/master/src/… - it should just create a new instance itself? What am I missing? As an aside - I only had to do this in .NET Core 3.1 (I migrated an app from 2.1 where this didn't matter).Elsa
Thank you @Luk, you have solved a mystery for me that I got when upgrading from 2.2 to 3.1 and again when creating a new 3.1 project. As @Elsa pointed out I couldn't see why on earth this would be an issue given that line in the code for InitializeAsync and until I read your comment I had been completely unable to work it out.Gravel
To avoid Null reference exception with Authentication 3.1 library , add on line number 2 in BasicAuthenticationTests() constructor following line _options.Setup(x => x.Get(AuthenticationSchemeOptions.AuthenticationSchemeName)).Returns(new AuthenticationSchemeOptions());Roughshod

© 2022 - 2024 — McMap. All rights reserved.