using AbpApplicationFactory.Create overwrites IConfiguration - abp

When using Abp to run a HostedService the IConfiguration created in the main method gets overwritten when using AbpApplicationFactory.Create
The main methods uses the default Microsoft implementation to connect to the AzureKeyvault:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging((context, logging) => logging.ClearProviders())
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<DbMigratorHostedService>();
})
.ConfigureAppConfiguration((context, config) =>
{
if (context.HostingEnvironment.IsProduction())
{
var builtConfig = config.Build();
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(
new KeyVaultClient.AuthenticationCallback(
azureServiceTokenProvider.KeyVaultTokenCallback));
config.AddAzureKeyVault(
$"https://{builtConfig["KeyVaultName"]}.vault.azure.net/",
keyVaultClient,
new DefaultKeyVaultSecretManager());
}
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
For now I am using the default DbMigrator that is added when creating an abp module:
public class DbMigratorHostedService : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
public DbMigratorHostedService(IHostApplicationLifetime hostApplicationLifetime)
{
_hostApplicationLifetime = hostApplicationLifetime;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
using var application = AbpApplicationFactory.Create<MyDbMigratorModule>(options =>
{
options.UseAutofac();
options.Services.AddLogging(c => c.AddSerilog());
});
application.Initialize();
await application
.ServiceProvider
.GetRequiredService<MyDbMigrationService>()
.MigrateAsync();
application.Shutdown();
_hostApplicationLifetime.StopApplication();
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
I can see that the AbpApplicationFactory.Create method also has options. However this doesn't have anything to configure the configuration. Why is the application scope not using the same IConfiguration from outside the scope ? Outside the scope i got 6 configuration providers including the key vault configuration and inside the scope there are only 2 missing the Keyvault one.
Am I missing something here ?
I also tried to get rid of the application scope, then i get the right configuration. However then I can't call the initialize method on the application and I can't use a startup class since it isn't a web application.

You are right. The startup template is not designed to handle such cases. I created an issue to fix it: https://github.com/abpframework/abp/issues/5006
For now, you can replace the configuration with your custom built one:
I din't try it, but this will also probably work (and it is exactly what you want):
inject IConfiguration to the hosted service.
Pass it to the ReplaceConfiguration.

Related

About load supported cultures from DB in .NET CORE

I have a Language entity with all supported languages in my db, each language has a culture string attribute. I want to load supported cultures from DB.
In my service initializer I have it:
public void ConfigureServices(IServiceCollection services)
{
// ... previous configuration not shown
services.Configure<RequestLocalizationOptions>(
opts =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-GB"),
new CultureInfo("en-US"),
new CultureInfo("en"),
new CultureInfo("fr-FR"),
new CultureInfo("fr"),
};
opts.DefaultRequestCulture = new RequestCulture("en-GB");
// Formatting numbers, dates, etc.
opts.SupportedCultures = supportedCultures;
// UI strings that we have localized.
opts.SupportedUICultures = supportedCultures;
});
}
How I can access my DB context inside it?
There is any other better way to do it?
I don't think there's an out of the box solution for this.
However, you can implement your own middleware that achieves this by using ASP.Net's RequestLocalizationMiddleware:
public class CustomRequestLocalizationMiddleware
{
private readonly RequestDelegate next;
private readonly ILoggerFactory loggerFactory;
public CustomRequestLocalizationMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
this.next = next;
this.loggerFactory = loggerFactory;
}
public async Task Invoke(HttpContext context /* You can inject services here, such as DbContext or IDbConnection*/)
{
// You can search your database for your supported and/or default languages here
// This query will execute for all requests, so consider using caching
var cultures = await Task.FromResult(new[] { "en" });
var defaultCulture = await Task.FromResult("en");
// You can configure the options here as you would do by calling services.Configure<RequestLocalizationOptions>()
var options = new RequestLocalizationOptions()
.AddSupportedCultures(cultures)
.AddSupportedUICultures(cultures)
.SetDefaultCulture(defaultCulture);
// Finally, we instantiate ASP.Net's default RequestLocalizationMiddleware and call it
var defaultImplementation = new RequestLocalizationMiddleware(next, Options.Create(options), loggerFactory);
await defaultImplementation.Invoke(context);
}
}
Then, we inject the required services and use the custom middleware in Startup.cs or Program.cs as follows:
services.AddLocalization()
/* ... */
app.UseMiddleware<CustomRequestLocalizationMiddleware>()
Do not call app.UseRequestLocalization(), because this would call ASP.Net's RequestLocalizationMiddleware again with the default options, and override the culture that has been resolved previously.

Identity Server 4 AddOidcStateDataFormatterCache does not apply to AddGoogle

When using the AddOidcStateDataFormatterCache method via:
services.AddOidcStateDataFormatterCache();
It only applies to providers which are added using
.AddOpenIdConnect();
Is there a way to apply the distributedCacheFormatter when using
.AddGoogle()
Google is also an OpenId Provider and can be added using .AddOpenIdConnect or .AddGoogle, but using .AddGoogle doesn't use the state data formatter. I confirmed this by checking the redis cache (used as the underlying implementation of IDistributedCache) and saw a key created "DistributedCacheStateDataFormatter..." when using .AddOpenIdConnect, but nothing is created when using .AddGoogle.
I'm thinking this might be because .AddGoogle might use a different authentication handler which doesn't get picked up automatically by AddOidcStateDataFormatterCache
This is because the GoogleOptions class inherits from OAuthOptions and not OpenIdConnectOptions but they both have a ISecureDataFormat<AuthenticationProperties> StateDataFormat so you could re-use the DistributedCacheStateDataFormatter provided by identityserver4
The post-configure class:
internal class ConfigureGoogleOptions : IPostConfigureOptions<GoogleOptions>
{
private string[] _schemes;
private readonly IHttpContextAccessor _httpContextAccessor;
public ConfigureGoogleOptions(string[] schemes, IHttpContextAccessor httpContextAccessor)
{
_schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
public void PostConfigure(string name, GoogleOptions options)
{
// no schemes means configure them all
if (_schemes.Length == 0 || _schemes.Contains(name))
{
options.StateDataFormat = new DistributedCacheStateDataFormatter(_httpContextAccessor, name);
}
}
}
And the registration helper (add this to your own static class):
public static IServiceCollection AddGoogleStateDataFormatterCache(this IServiceCollection services, params string[] schemes)
{
services.AddSingleton<IPostConfigureOptions<GoogleOptions>>(
svcs => new ConfigureGoogleOptions(
schemes,
svcs.GetRequiredService<IHttpContextAccessor>())
);
return services;
}

.NET Core 3 preview: Synchronous operations are disallowed

I have an Angular.js app that I am porting to .NET Core.
It was working fine in the previous version of .NET Core 3 preview; 3.2.
However, after upgrading to latest 3.3 some of the get requests are returning this error:
InvalidOperationException: Synchronous operations are disallowed. Call
WriteAsync or set AllowSynchronousIO to true instead.
I can't see why this is happening with only some requests and not others.
I believe that by default Angular.js does async: xhr.open(method, url, true);
Can anyone shed some light on this?
This problem is described here: https://github.com/aspnet/AspNetCore/issues/8302
The workaround for now is to manually set AllowSynchronous to true in startup.cs;
// Startup.ConfigureServices
services.Configure<IISServerOptions>(options =>
{
options.AllowSynchronousIO = true;
});
It's worth noting that if you host on kestrel directly then your Program.cs should have appropriate ConfigureKestrel call
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.AllowSynchronousIO = true;
})
you can disable it for a special method
var syncIOFeature = HttpContext.Features.Get<IHttpBodyControlFeature>();
if (syncIOFeature != null)
{
syncIOFeature.AllowSynchronousIO = true;
}
or disable in all application scope
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.AllowSynchronousIO = true;
})
or in service configure startup
services.Configure<IISServerOptions>(options =>
{
options.AllowSynchronousIO = true;
});
If you are using a CustomWebApplicationFactory like me, you can set the flag in its constructor, It makes my test direct from VS2019 works.
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class
{
public CustomWebApplicationFactory()
{
Server.AllowSynchronousIO = true;
}

Protect IdentityServer4 Api with itself

We are currently working on a identityserver4 implementation which will also have a few api calls.
Those api calls should only be available if a user is authorized(with the bearer token).
In the Startup.cs we have the services.AddIdentityServer() since this is the identityServer, and also added the AddAuthentication() call to make sure the authorized endpoints are only available for authorized connections.
Startup.cs => ConfigureServices():
services.AddIdentityServer();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:9000";
options.RequireHttpsMetadata = false;
options.ApiName = "identityserver4";
});
Startup.cs => Configure():
app.UseAuthentication();
app.UseIdentityServer();
//app.UseAuthentication();
using the UseAuthentication() before or after UseIdentityServer() does not change anything.
My api call within the identityserver is still avaialble to all.
Currently using postman to test the calls.
Do i need to add something? Is there something i missed?
Kind regards,
Walter
edit 1: added controller and full startup.cs
UserController.cs:
namespace Identity.Controllers
{
[Authorize]
[Route("[controller]")]
public class UserController : ControllerBase
{
private readonly ILogger _logger;
private readonly IUserBusinessLogic _userBusinessLogic;
public UserController(ILogger<UserController> logger, IUserBusinessLogic userBusinessLogic)
: base()
{
_logger = logger;
_userBusinessLogic = userBusinessLogic;
}
[Route("")]
[HttpGet]
public async Task<ActionResult<IList<UserDto>>> GetAllUsers()
{
var users = await _userBusinessLogic.GetAll();
return users.ToList();
}
}
}
Startup.cs:
namespace Identity
{
public class Startup
{
private readonly IConfiguration _configuration;
private readonly ILogger _logger;
public Startup(IConfiguration configuration, ILogger<Startup> logger)
: base()
{
_configuration = configuration;
_logger = logger;
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddJsonFormatters()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddRazorViewEngine();
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context => new ValidationProblemDetailsResult();
});
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:9000";
options.RequireHttpsMetadata = false;
options.ApiName = "identityserver4";
});
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddPersistedGrantStore<PersistedGrantStoreBusinessLogic>()
.AddResourceStore<ResourceBusinessLogic>()
.AddClientStore<ClientBusinessLogic>()
.AddProfileService<ProfileBusinessLogic>()
.AddCorsPolicyService<CorsPolicyBusinessLogic>();
services.AddCors(options =>
{
options.AddPolicy("default",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader().Build());
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseCors("default");
app.UseIdentityServer();
app.UseStaticFiles();
app.ConfigureExceptionHandler(_logger);
app.UseMvcWithDefaultRoute();
}
}
}
I just wrote some code for the exact same purpose, and I struggled with the same issues as you do.
According to the Identity Server Doc, do not forget to specify the authentication scheme in [Authorize] attribute.
Startup.cs:
services.AddAuthentication()
.AddIdentityServerAuthentication("Bearer", options =>
{
options.Authority = "http://localhost:9000";
options.RequireHttpsMetadata = false;
options.ApiName = "identityserver4";
});
Note that "Bearer" is given to AddIdentityServerAuthentication and not to AddAuthentication.
Controller.cs:
[Authorize(AuthenticationSchemes = "Bearer")]
public IActionResult YourControllerAction()
{
}
Hope it will works for you!
Found my problem!
in my startup i used services.AddMvcCore() when i should have used services.AddMvc() OR just add the services.AddAuthorization which is not added using services.AddMvcCore().
I came upon this solution after doing some research for something else. In my research i came upon this page: https://offering.solutions/blog/articles/2017/02/07/difference-between-addmvc-addmvcore/
It explains the differences between AddMvc() and AddMvcCore().
So after adding services.AddAuthorization() my problem was solved and the api within my identityserver where protected.
Thank you to all who tried to help me!
this answer may comes late but comes late better than never , using IdentityServer to secure other APIs and do not secure the main token or access provider may seems silly somehow ,so in this case if you want to secure the Api That implement IdentityServer it self you can add the predefined IdentityServer scope IdentityServerApi in the allowed scopes and also for the client scopes , and then you have to configure the services to use the local authentication (provided by identityserver) by adding services.AddLocalApiAuthentication();
and the final part is to add the authorize attribute to the controller or the action method as you wish as follow [Authorize(Policy = LocalApi.PolicyName)]
and in the end you can add claims policy authorization side by side with the local authentication

Integration testing with in-memory IdentityServer

I have an API that uses IdentityServer4 for token validation.
I want to unit test this API with an in-memory TestServer. I'd like to host the IdentityServer in the in-memory TestServer.
I have managed to create a token from the IdentityServer.
This is how far I've come, but I get an error "Unable to obtain configuration from http://localhost:54100/.well-known/openid-configuration"
The Api uses [Authorize]-attribute with different policies. This is what I want to test.
Can this be done, and what am I doing wrong?
I have tried to look at the source code for IdentityServer4, but have not come across a similar integration test scenario.
protected IntegrationTestBase()
{
var startupAssembly = typeof(Startup).GetTypeInfo().Assembly;
_contentRoot = SolutionPathUtility.GetProjectPath(#"<my project path>", startupAssembly);
Configure(_contentRoot);
var orderApiServerBuilder = new WebHostBuilder()
.UseContentRoot(_contentRoot)
.ConfigureServices(InitializeServices)
.UseStartup<Startup>();
orderApiServerBuilder.Configure(ConfigureApp);
OrderApiTestServer = new TestServer(orderApiServerBuilder);
HttpClient = OrderApiTestServer.CreateClient();
}
private void InitializeServices(IServiceCollection services)
{
var cert = new X509Certificate2(Path.Combine(_contentRoot, "idsvr3test.pfx"), "idsrv3test");
services.AddIdentityServer(options =>
{
options.IssuerUri = "http://localhost:54100";
})
.AddInMemoryClients(Clients.Get())
.AddInMemoryScopes(Scopes.Get())
.AddInMemoryUsers(Users.Get())
.SetSigningCredential(cert);
services.AddAuthorization(options =>
{
options.AddPolicy(OrderApiConstants.StoreIdPolicyName, policy => policy.Requirements.Add(new StoreIdRequirement("storeId")));
});
services.AddSingleton<IPersistedGrantStore, InMemoryPersistedGrantStore>();
services.AddSingleton(_orderManagerMock.Object);
services.AddMvc();
}
private void ConfigureApp(IApplicationBuilder app)
{
app.UseIdentityServer();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
var options = new IdentityServerAuthenticationOptions
{
Authority = _appsettings.IdentityServerAddress,
RequireHttpsMetadata = false,
ScopeName = _appsettings.IdentityServerScopeName,
AutomaticAuthenticate = false
};
app.UseIdentityServerAuthentication(options);
app.UseMvc();
}
And in my unit-test:
private HttpMessageHandler _handler;
const string TokenEndpoint = "http://localhost/connect/token";
public Test()
{
_handler = OrderApiTestServer.CreateHandler();
}
[Fact]
public async Task LeTest()
{
var accessToken = await GetToken();
HttpClient.SetBearerToken(accessToken);
var httpResponseMessage = await HttpClient.GetAsync("stores/11/orders/asdf"); // Fails on this line
}
private async Task<string> GetToken()
{
var client = new TokenClient(TokenEndpoint, "client", "secret", innerHttpMessageHandler: _handler);
var response = await client.RequestClientCredentialsAsync("TheMOON.OrderApi");
return response.AccessToken;
}
You were on the right track with the code posted in your initial question.
The IdentityServerAuthenticationOptions object has properties to override the default HttpMessageHandlers it uses for back channel communication.
Once you combine this with the CreateHandler() method on your TestServer object you get:
//build identity server here
var idBuilder = new WebBuilderHost();
idBuilder.UseStartup<Startup>();
//...
TestServer identityTestServer = new TestServer(idBuilder);
var identityServerClient = identityTestServer.CreateClient();
var token = //use identityServerClient to get Token from IdentityServer
//build Api TestServer
var options = new IdentityServerAuthenticationOptions()
{
Authority = "http://localhost:5001",
// IMPORTANT PART HERE
JwtBackChannelHandler = identityTestServer.CreateHandler(),
IntrospectionDiscoveryHandler = identityTestServer.CreateHandler(),
IntrospectionBackChannelHandler = identityTestServer.CreateHandler()
};
var apiBuilder = new WebHostBuilder();
apiBuilder.ConfigureServices(c => c.AddSingleton(options));
//build api server here
var apiClient = new TestServer(apiBuilder).CreateClient();
apiClient.SetBearerToken(token);
//proceed with auth testing
This allows the AccessTokenValidation middleware in your Api project to communicate directly with your In-Memory IdentityServer without the need to jump through hoops.
As a side note, for an Api project, I find it useful to add IdentityServerAuthenticationOptions to the services collection in Startup.cs using TryAddSingleton instead of creating it inline:
public void ConfigureServices(IServiceCollection services)
{
services.TryAddSingleton(new IdentityServerAuthenticationOptions
{
Authority = Configuration.IdentityServerAuthority(),
ScopeName = "api1",
ScopeSecret = "secret",
//...,
});
}
public void Configure(IApplicationBuilder app)
{
var options = app.ApplicationServices.GetService<IdentityServerAuthenticationOptions>()
app.UseIdentityServerAuthentication(options);
//...
}
This allows you to register the IdentityServerAuthenticationOptions object in your tests without having to alter the code in the Api project.
I understand there is a need for a more complete answer than what #james-fera posted. I have learned from his answer and made a github project consisting of a test project and API project. The code should be self-explanatory and not hard to understand.
https://github.com/emedbo/identityserver-test-template
The IdentityServerSetup.cs class https://github.com/emedbo/identityserver-test-template/blob/master/tests/API.Tests/Config/IdentityServerSetup.cs can be abstracted away e.g. NuGetted away, leaving the base class IntegrationTestBase.cs
The essences is that can make the test IdentityServer work just like a normal IdentityServer, with users, clients, scopes, passwords etc. I have made the DELETE method [Authorize(Role="admin)] to prove this.
Instead of posting code here, I recommend read #james-fera's post to get the basics then pull my project and run tests.
IdentityServer is such a great tool, and with the ability to use the TestServer framework it gets even better.
I think you probably need to make a test double fake for your authorization middleware depending on how much functionality you want. So basically you want a middleware that does everything that the Authorization middleware does minus the back channel call to the discovery doc.
IdentityServer4.AccessTokenValidation is a wrapper around two middlewares. The JwtBearerAuthentication middleware, and the OAuth2IntrospectionAuthentication middleware. Both of these grab the discovery document over http to use for token validation. Which is a problem if you want to do an in-memory self-contained test.
If you want to go through the trouble you will probably need to make a fake version of app.UseIdentityServerAuthentication that doesnt do the external call that fetches the discovery document. It only populates the HttpContext principal so that your [Authorize] policies can be tested.
Check out how the meat of IdentityServer4.AccessTokenValidation looks here. And follow up with a look at how JwtBearer Middleware looks here
We stepped away from trying to host a mock IdentityServer and used dummy/mock authorizers as suggested by others here.
Here's how we did that in case it's useful:
Created a function which takes a type, creates a test Authentication Middleware and adds it to the DI engine using ConfigureTestServices (so that it's called after the call to Startup.)
internal HttpClient GetImpersonatedClient<T>() where T : AuthenticationHandler<AuthenticationSchemeOptions>
{
var _apiFactory = new WebApplicationFactory<Startup>();
var client = _apiFactory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, T>("Test", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test");
return client;
}
Then we create what we called 'Impersonators' (AuthenticationHandlers) with the desired roles to mimic users with roles (We actually used this as a base class, and create derived classes based on this to mock different users):
public abstract class FreeUserImpersonator : AuthenticationHandler<AuthenticationSchemeOptions>
{
public Impersonator(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
base.claims.Add(new Claim(ClaimTypes.Role, "FreeUser"));
}
protected List<Claim> claims = new List<Claim>();
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
Finally, we can perform our integration tests as follows:
// Arrange
HttpClient client = GetImpersonatedClient<FreeUserImpersonator>();
// Act
var response = await client.GetAsync("api/things");
// Assert
Assert.That.IsSuccessful(response);
Test API startup:
public class Startup
{
public static HttpMessageHandler BackChannelHandler { get; set; }
public void Configuration(IAppBuilder app)
{
//accept access tokens from identityserver and require a scope of 'Test'
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost",
BackchannelHttpHandler = BackChannelHandler,
...
});
...
}
}
Assigning the AuthServer.Handler to TestApi BackChannelHandler in my unit test project:
protected TestServer AuthServer { get; set; }
protected TestServer MockApiServer { get; set; }
protected TestServer TestApiServer { get; set; }
[OneTimeSetUp]
public void Setup()
{
...
AuthServer = TestServer.Create<AuthenticationServer.Startup>();
TestApi.Startup.BackChannelHandler = AuthServer.CreateHandler();
TestApiServer = TestServer.Create<TestApi.Startup>();
}
The trick is to create a handler using the TestServer that is configured to use IdentityServer4. Samples can be found here.
I created a nuget-package available to install and test using the Microsoft.AspNetCore.Mvc.Testing library and the latest version of IdentityServer4 for this purpose.
It encapsulates all the infrastructure code necessary to build an appropriate WebHostBuilder which is then used to create a TestServer by generating the HttpMessageHandler for the HttpClient used internally.
None of the other answers worked for me because they rely on 1) a static field to hold your HttpHandler and 2) the Startup class to have knowledge that it may be given a test handler. I've found the following to work, which I think is a lot cleaner.
First create an object that you can instantiate before your TestHost is created. This is because you won't have the HttpHandler until after the TestHost is created, so you need to use a wrapper.
public class TestHttpMessageHandler : DelegatingHandler
{
private ILogger _logger;
public TestHttpMessageHandler(ILogger logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_logger.Information($"Sending HTTP message using TestHttpMessageHandler. Uri: '{request.RequestUri.ToString()}'");
if (WrappedMessageHandler == null) throw new Exception("You must set WrappedMessageHandler before TestHttpMessageHandler can be used.");
var method = typeof(HttpMessageHandler).GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic);
var result = method.Invoke(this.WrappedMessageHandler, new object[] { request, cancellationToken });
return await (Task<HttpResponseMessage>)result;
}
public HttpMessageHandler WrappedMessageHandler { get; set; }
}
Then
var testMessageHandler = new TestHttpMessageHandler(logger);
var webHostBuilder = new WebHostBuilder()
...
services.PostConfigureAll<JwtBearerOptions>(options =>
{
options.Audience = "http://localhost";
options.Authority = "http://localhost";
options.BackchannelHttpHandler = testMessageHandler;
});
...
var server = new TestServer(webHostBuilder);
var innerHttpMessageHandler = server.CreateHandler();
testMessageHandler.WrappedMessageHandler = innerHttpMessageHandler;

Resources