Skip to content

Instantly share code, notes, and snippets.

@jelster
Last active August 2, 2021 06:50
Show Gist options
  • Save jelster/6b850d567acef343e59fad70cb20455a to your computer and use it in GitHub Desktop.
Save jelster/6b850d567acef343e59fad70cb20455a to your computer and use it in GitHub Desktop.
ASP.NET Core 2.1 - Setting up functional integration tests with authentication middleware
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();
}
}
}
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
});
}
}
}
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);
}
}
}
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 => { });
});
}
}
}
@jelster
Copy link
Author

jelster commented Feb 20, 2020

Since the bulk of the work is done in the standard service initialization fashion, I don't see why you couldn't use this in a razor pages app. I don't have any examples of the sort you're looking for, but I think there's some samples in the MSFT docs that do cover this topic explicitly. You could try starting with those and adapting this code into them - love to see what you come up with!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment