Unit tests are extremely important in making sure your application behaves normally. There is often the need to unit test a method which requires an authenticated context and lucky there is an easy way to do this.
A typical startup file may have the authorization and authentication middleware setup. I know it might seem confusing (WebApi which is usually stateless and Authentication) but it’s possible (using OpenIdConnect) and in many cases actually required (e.g. a mobile application with user accounts that has a webapi backend).
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddAuthentication(); services.AddAuthorization(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
When I want to figure out how to design something, I often start by checking the underlying structure. And for authentication there extension AddAuthentication will register some services that we can use for our test environment
As you can see here, we have some providers which are used internally to determine which scheme to execute and for each scheme which handler is provided.
Our solution looks like this:
Now, the classes are pretty simple. We are replacing the scheme provider and make it return a custom handler. In this custom handler we will inject a user that we need for testing
The user definition:
public class IntegrationUser { public string Name { get; } public Guid UserId { get; } public IntegrationUser(string name, Guid userId) { this.Name = name; this.UserId = userId; } }
The user provider service:
public interface IIntegrationUserProvider { public IntegrationUser Current { get; } void SetUser(IntegrationUser user); }
The user provider service implementation:
public class IntegrationUserProvider : IIntegrationUserProvider { public IntegrationUser Current { get; private set; } public IntegrationUserProvider() { } public void SetUser(IntegrationUser user) { Current = user; } }
The authentication handler where we use our user provider to create the ClaimsPrincipal:
public class IntegrationAuthenticationHandler : AuthenticationHandler { public const string FakeSchemeName = "IntegrationTestFakeScheme"; public IntegrationAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IIntegrationUserProvider integrationUserProvider) : base(options, logger, encoder, clock) { IntegrationUserProvider = integrationUserProvider; } public IIntegrationUserProvider IntegrationUserProvider { get; } protected override Task HandleChallengeAsync(AuthenticationProperties properties) { Response.StatusCode = 200; return Task.CompletedTask; } protected override Task HandleAuthenticateAsync() { if (IntegrationUserProvider.Current is null) { return Task.FromResult(AuthenticateResult.Fail("")); } IntegrationUser user = IntegrationUserProvider.Current; var claims = new List(); claims.Add(new Claim(ClaimTypes.Name, user.Name)); claims.Add(new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString())); ClaimsIdentity id = new ClaimsIdentity(claims, "Custom"); var suc = AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(id), IntegrationAuthenticationHandler.FakeSchemeName)); return Task.FromResult(suc); } }
For the AuthenticationOptions we are defining an empty class
public class IntegrationAuthenticationOptions : AuthenticationSchemeOptions { }
And finally the scheme provider which always returns the custom handler
public sealed class IntegrationSchemeProvider : IAuthenticationSchemeProvider { private static AuthenticationScheme FakeScheme = new AuthenticationScheme( IntegrationAuthenticationHandler.FakeSchemeName, IntegrationAuthenticationHandler.FakeSchemeName, typeof(IntegrationAuthenticationHandler)); private static readonly IEnumerable FakeSchemes = new List { FakeScheme }; public void AddScheme(AuthenticationScheme scheme) { } public Task<IEnumerable> GetAllSchemesAsync() => Task.FromResult(FakeSchemes); public Task GetDefaultAuthenticateSchemeAsync() => Task.FromResult(FakeScheme); public Task GetDefaultChallengeSchemeAsync() => Task.FromResult(FakeScheme); public Task GetDefaultForbidSchemeAsync() => Task.FromResult(FakeScheme); public Task GetDefaultSignInSchemeAsync() => Task.FromResult(FakeScheme); public Task GetDefaultSignOutSchemeAsync() => Task.FromResult(FakeScheme); public Task<IEnumerable> GetRequestHandlerSchemesAsync() => Task.FromResult(FakeSchemes); public Task GetSchemeAsync(string name) => Task.FromResult(FakeScheme); public void RemoveScheme(string name) { } }
We are almost set. Now we have to use the application factory to create a client for our api and replace the Scheme provider service
P.S. Don’t forget to add the real scheme to the api controller. The framework needs to now that a scheme is used.