Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you configure ASP.Net TestHost to work with OpenId Connect?

I have an ASP.Net Core application configured to issue and authenticate JWT bearer tokens. Clients are able to successfully retrieve bearer tokens and authenticate with the token when the site is hosted in Kestrel.

I also have a suite of integration tests which use Microsoft.AspNetCore.TestHost.TestServer. Prior to adding authentication, the tests were able to successfully make requests against the application. After adding authentication, I started getting errors pertaining to accessing open id configuration. The specific exception I'm seeing is this:

info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://  
fail: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware[3]
      Exception occurred while processing message.
System.InvalidOperationException: IDX10803: Unable to obtain configuration from: 'http://localhost/.well-known/openid-configuration'. ---> System.IO.IOException: IDX10804: Unable to retrieve document from: 'http://localhost/.well-known/openid-configuration'. ---> System.Net.Http.HttpRequestException: Response status code does not indicate success: 404 (Not Found).

Based on my research, this is sometimes triggered when the Authority is set to a different host than the hosting server. For instance, Kestrel runs at http://localhost:5000 by default which is what I had my Authority set to initially, but upon setting it to what the TestServer is emulating (http://localhost), it still gives the same error. Here is my authentication configuration:

    app.UseJwtBearerAuthentication(new JwtBearerOptions
    {
        AutomaticAuthenticate = true,
        AutomaticChallenge = true,
        RequireHttpsMetadata = false,
        TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = signingKey,
            ValidateAudience = true
        },
        Audience = "Anything",
        Authority = "http://localhost"
    });

What is odd is that attempting to hit the URL directly from the Integration test works fine:

enter image description here

So how do you configure the ASP.Net TestServer and OpenId Connect infrastructure to work together?

=== EDIT ====

In reflecting on this a bit, it occurred to me that the issue is that the JWT authorization internals is trying to make a request to http://localhost port 80, but it isn't trying to make the request using the TestServer and is therefore looking for a real server. Since there isn't one, it's never going to authenticate. It looks like the next step is to see if there is some way to turn off the Authority check or extend the infrastructure in some way to allow for it to use the TestServer as the host.

like image 708
Derek Greer Avatar asked Nov 08 '22 07:11

Derek Greer


1 Answers

The JWT infrastructure does indeed try to make an HTTP request by default. I was able to get it working by setting the JwtBearerOptions.ConfigurationManager property to a new instance of OpenIdConnectionConfigurationRetriever() which supplied an IDocumentResolver provided by DI:

        ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
            authority + "/.well-known/openid-configuration",
            new OpenIdConnectConfigurationRetriever(),
            _documentRetriever),

In the production code, I just register the default with my container (Autofac):

builder.RegisterType<HttpDocumentRetriever>().As<IDocumentRetriever>();

I was already using a derived Setup class for my integration tests which follows the Template Method pattern to configure the container, so I was able to override the IDocumentRetriever instance with one that returns the results from the TestServer instance.

I did run into an additional snag which is that the TestServer's client seemed to hang when a request was made (the one initiated from JWT calling my IDocumentRetriever) while another request was already outstanding (the one that initiated the request to begin with), so I had to make the request beforehand and supply the cached results from my IDocumentRetriever:

public class TestServerDocumentRetriever : IDocumentRetriever
{
    readonly IOpenIdConfigurationAccessor _openIdConfigurationAccessor;

    public TestServerDocumentRetriever(IOpenIdConfigurationAccessor openIdConfigurationAccessor)
    {
        _openIdConfigurationAccessor = openIdConfigurationAccessor;
    }

    public Task<string> GetDocumentAsync(string address, CancellationToken cancel)
    {
        return Task.FromResult(_openIdConfigurationAccessor.GetOpenIdConfiguration());
    }
}
like image 193
Derek Greer Avatar answered Nov 26 '22 04:11

Derek Greer