ASP.NET Core 2.1 - Setting up functional integration tests with authentication middleware

Josh Elster
2019-01-08T17:38:58Z

BasicTests.cs

| using Microsoft.AspNetCore.Mvc.Testing;
| using System.Net;
| using System.Threading.Tasks;
| using Xunit;
| using System.Net.Http;
| using System.Collections.Generic;
| using Newtonsoft.Json;
|
| namespace Api.FunctionalTests
| {
| public class BasicTests : IClassFixture<TestWebApplicationFactory<MyWebApi.Startup>>
| {
| private readonly HttpClient _client;
| private readonly HttpClient _unauthorizedClient;
|
| private readonly TestWebApplicationFactory<MyWebApi.Startup> _factory;
|
| public BasicTests(TestWebApplicationFactory<MyWebApi.Startup> factory)
| {
| _factory = factory;
|
| _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
| {
| AllowAutoRedirect = false
| });
|
| // Requests made using this HTTP client will go through the test scheme handler...
| _client.DefaultRequestHeaders.Add("X-Test-Auth", "false");
| // and be considered authenticated
| _client.DefaultRequestHeaders.Add("Authorization", "Bearer asdf1234qwerty");
|
| // this client makes requests that end up being handled by the "real" auth middleware
| _unauthorizedClient = _factory.CreateClient();
| }
| }
| }

ClaimsTests.cs

| using Microsoft.AspNetCore.Mvc.Testing;
| using System.Net;
| using System.Threading.Tasks;
| using Xunit;
| using System.Net.Http;
| using System.Collections.Generic;
| using Newtonsoft.Json;
|
| namespace Api.FunctionalTests
| {
| public class ClaimsTests : IClassFixture<TestWebApplicationFactory<MyWebApi.Startup>>
| {
| private readonly HttpClient _client;
| private readonly HttpClient _unauthorizedClient;
|
| private readonly TestWebApplicationFactory<MyWebApi.Startup> _factory;
|
| public ClaimsTests(TestWebApplicationFactory<MyWebApi.Startup> factory)
| {
| _factory = factory;
| _factory = _factory.WithWebHostBuilder(builder =>
| {
| builder.ConfigureServices(services =>
| {
| services.AddAuthentication(o =>
| {
| o.DefaultScheme = "TestAuthenticationScheme";
| })
| .AddTestAuthentication("TestAuthenticationScheme", "Test Auth", o =>
| {
| o.Identity = new ClaimsIdentity( /* pass in a Claim[] */);
| });
| });
| });
| _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
| {
| AllowAutoRedirect = false
| });
| }
| }
| }

TestAuthMiddleware.cs

| using Microsoft.AspNetCore.Authentication;
| using System.Security.Claims;
| using System.Threading.Tasks;
| using System.Text.Encodings.Web;
| using Microsoft.Extensions.Options;
| using Microsoft.Extensions.Logging;
| using System;
| using System.Linq;
|
| namespace Api.FunctionalTests
| {
| public class TestAuthHandler : AuthenticationHandler<TestAuthenticationOptions>
| {
| public TestAuthHandler(IOptionsMonitor<TestAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
| : base(options, logger, encoder, clock)
| {
| }
|
| protected override Task<AuthenticateResult> HandleAuthenticateAsync()
| {
| // if the header is provided and its value is 'true', then step aside and let the regular auth handle it
| // a value of <anything other than 'true'> will bypass the rest of the auth pipeline
| if (!Context.Request.Headers.TryGetValue("X-Test-Auth", out var header) || header.Contains("true"))
| {
| return Task.FromResult(AuthenticateResult.NoResult());
| }
| // value is false (or anything other than 'true'). Make sure they've made the effort to provide the auth header
| // but don't bother to check the value
| if (header.Any() && !Context.Request.Headers.TryGetValue("Authorization", out var authHeader))
| {
| return Task.FromResult(AuthenticateResult.NoResult());
| }
| // otherwise, here's your auth ticket!
| return Task.FromResult(
| AuthenticateResult.Success(
| new AuthenticationTicket(
| new ClaimsPrincipal(Options.Identity),
| new AuthenticationProperties(),
| this.Scheme.Name)));
|
| }
| }
| public class TestAuthenticationOptions : AuthenticationSchemeOptions
| {
| public bool RequireBearerToken { get; set; }
| // customize as needed
| public virtual ClaimsIdentity Identity { get; set; } = new ClaimsIdentity(
| new Claim[]
| {
| new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", Guid.NewGuid().ToString()),
| new Claim("http://schemas.microsoft.com/identity/claims/tenantid", "test"),
| new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", Guid.NewGuid().ToString()),
| new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", "test"),
| new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", "test"),
| new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", "test"),
| },
| "test");
|
| public TestAuthenticationOptions() {}
| }
|
|
| public static class TestAuthenticationExtensions
| {
| public static AuthenticationBuilder AddTestAuthentication(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TestAuthenticationOptions> configureOptions)
| {
| return builder.AddScheme<TestAuthenticationOptions, TestAuthHandler>(authenticationScheme, configureOptions);
| }
| }
| }

TestWebApplicationFactory.cs

| using Microsoft.AspNetCore.Hosting;
| using Microsoft.AspNetCore.Mvc.Testing;
| using Microsoft.AspNetCore.TestHost;
| using Microsoft.Extensions.DependencyInjection;
| using System.Linq;
| using Microsoft.Extensions.Configuration;
|
| namespace Api.FunctionalTests
| {
| public class TestWebApplicationFactory<TStartup> : WebApplicationFactory<MyWebApi.Startup>
| {
| protected override void ConfigureWebHost(IWebHostBuilder builder)
| {
| builder.UseEnvironment("Integration");
| builder.ConfigureAppConfiguration((builderContext, config) =>
| {
| IHostingEnvironment env = builderContext.HostingEnvironment;
| config.Sources.Clear();
| config.AddJsonFile($"appsettings.{env.EnvironmentName}.json", false, reloadOnChange: true);
| });
| builder.ConfigureTestServices(services =>
| {
| services
| .AddAuthentication(o =>
| {
| o.DefaultScheme = "TestAuthenticationScheme";
| }).AddTestAuthentication("TestAuthenticationScheme", "Test Auth", o => { });
| });
| }
| }
| }