Integration tests often need to test endpoints that require an authenticated user. The trick is to replace the IAuthenticationSchemeProvider with a fake that always returns a custom handler — injecting whatever user the test needs, without touching JWTs or identity servers.
A typical startup with auth middleware:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication();
services.AddAuthorization();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}Looking at what AddAuthentication registers internally reveals the providers we can replace:

The solution structure:

A simple user model and an injectable provider that the test sets before each call:
public class IntegrationUser
{
public string Name { get; }
public Guid UserId { get; }
public IntegrationUser(string name, Guid userId)
{
Name = name;
UserId = userId;
}
}public interface IIntegrationUserProvider
{
IntegrationUser Current { get; }
void SetUser(IntegrationUser user);
}
public class IntegrationUserProvider : IIntegrationUserProvider
{
public IntegrationUser Current { get; private set; }
public void SetUser(IntegrationUser user)
{
Current = user;
}
}The authentication handler reads the current user from the provider and builds a ClaimsPrincipal:
public class IntegrationAuthenticationHandler
: AuthenticationHandler<IntegrationAuthenticationOptions>
{
public const string FakeSchemeName = "IntegrationTestFakeScheme";
public IIntegrationUserProvider IntegrationUserProvider { get; }
public IntegrationAuthenticationHandler(
IOptionsMonitor<IntegrationAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IIntegrationUserProvider integrationUserProvider)
: base(options, logger, encoder, clock)
{
IntegrationUserProvider = integrationUserProvider;
}
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = 200;
return Task.CompletedTask;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (IntegrationUserProvider.Current is null)
return Task.FromResult(AuthenticateResult.Fail("No user set"));
IntegrationUser user = IntegrationUserProvider.Current;
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString())
};
var identity = new ClaimsIdentity(claims, "Custom");
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), FakeSchemeName);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
public class IntegrationAuthenticationOptions : AuthenticationSchemeOptions { }The scheme provider always returns the fake handler regardless of which scheme is requested:
public sealed class IntegrationSchemeProvider : IAuthenticationSchemeProvider
{
private static readonly AuthenticationScheme FakeScheme = new AuthenticationScheme(
IntegrationAuthenticationHandler.FakeSchemeName,
IntegrationAuthenticationHandler.FakeSchemeName,
typeof(IntegrationAuthenticationHandler));
private static readonly IEnumerable<AuthenticationScheme> FakeSchemes
= new List<AuthenticationScheme> { FakeScheme };
public void AddScheme(AuthenticationScheme scheme) { }
public void RemoveScheme(string name) { }
public Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync()
=> Task.FromResult(FakeSchemes);
public Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
=> Task.FromResult(FakeScheme);
public Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync()
=> Task.FromResult(FakeScheme);
public Task<AuthenticationScheme> GetDefaultForbidSchemeAsync()
=> Task.FromResult(FakeScheme);
public Task<AuthenticationScheme> GetDefaultSignInSchemeAsync()
=> Task.FromResult(FakeScheme);
public Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync()
=> Task.FromResult(FakeScheme);
public Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync()
=> Task.FromResult(FakeSchemes);
public Task<AuthenticationScheme> GetSchemeAsync(string name)
=> Task.FromResult(FakeScheme);
}Wire it up in the WebApplicationFactory by replacing the scheme provider service:

Don't forget to add the real authentication scheme to the API controller — the framework needs to know a scheme is in use:

