Mock JwtBearerOptions BackendChannel for integration tests

Viewed 113

In my app I use following code to add :

authBuilder
    .AddJwtBearer("MyCustomScheme", options =>
    {
        options.Authority = "https://auth.example.com";
        options.AutomaticRefreshInterval = new TimeSpan(24, 0, 0);
    });

In my integration tests project I have following code:

public class TestAppFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        // tried also with .ConfigureTestServices
        builder.ConfigureServices(services =>
        {
            services.PostConfigure<JwtBearerOptions>("MyCustomScheme", options =>
            {
                // this is executed and options.Authority is auth.example.com here

                options.MetadataAddress = "https://inmemory.microsoft.com/common/.well-known/openid-configuration";
                options.Authority = "https://inmemory.microsoft.com/common/.well-known/openid-configuration";
                options.BackchannelHttpHandler = new MockBackchannel();
            });
        });
    }
}

And MockBackendChannel looks like this:

public class MockBackchannel : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.RequestUri.AbsoluteUri.Equals("https://inmemory.microsoft.com/common/.well-known/openid-configuration"))
        {
            return await EmbeddedResourceReader.GetOpenIdConfigurationAsResponseMessage("microsoft-openid-config.json");
        }
        if (request.RequestUri.AbsoluteUri.Equals("https://inmemory.microsoft.com/common/discovery/keys"))
        {
            return await EmbeddedResourceReader.GetOpenIdConfigurationAsResponseMessage("microsoft-wellknown-keys.json");
        }

        throw new NotImplementedException();
    }
}

I checked that services.PostConfigure<JwtBearerOptions> is invoked correctly on options set in the app, however when I am calling authorized API in integration test (with client created with Application.CreateClient()) I am getting following exception:

System.InvalidOperationException: IDX20803: Unable to obtain configuration from: 'System.String'.
 ---> System.IO.IOException: IDX20804: Unable to retrieve document from: 'System.String'.
 ---> System.Net.Http.HttpRequestException: nodename nor servname provided, or not known (auth.example.com:443)
 ---> System.Net.Sockets.SocketException (0xFFFDFFFF): nodename nor servname provided, or not known

So for some reason post-configuration has no impact on the scheme and it is still trying to call auth.example.com. How can I make this work?

1 Answers

It looks like you were following the same tutorial as me.

From what I could see in aspnet's source code, there is a PostConfiguration that is supposed to take the back channel and create a client from it. My guess is that our PostConfiguration is run after that one, so the client never uses our back channel.

A workaround that fixed my issue is to copy paste the code from that PostConfigure into my PostConfigure, so that we initialize the whole client:

services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
    options.BackchannelHttpHandler = new MockBackchannel();
    options.Backchannel = new HttpClient(options.BackchannelHttpHandler);
    options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core JwtBearer handler");
    options.Backchannel.Timeout = options.BackchannelTimeout;
    options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
    options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.MetadataAddress, new OpenIdConnectConfigurationRetriever(),
        new HttpDocumentRetriever(options.Backchannel) { RequireHttps = options.RequireHttpsMetadata })
    {
        RefreshInterval = options.RefreshInterval,
        AutomaticRefreshInterval = options.AutomaticRefreshInterval,
    };
});
Related