Using IdentityServer4 with WebApplicationFactory Integration Tests

Viewed 535

I am trying to get my integration tests working against a protected api endpoint. My test call the IDS connect/token endpoint and gets a valid token. When I use that to call the protected api I always get a failure with invalid_token. (the api is local to the IDS too)

If I manually debug the IDS and use postman to get a token and then manually set it and call the protected api with postman it works fine.

I suspect that the internals of the IDS validation code can't hit the discovery endpoint because it is spinning up its own HttpClient. I know that the WebApplicationFactory Client is a special implementation of HttpClient.

Is there a way that I can inject the WebApplicationFactory client into the IDS during configuration/startup so that it will work?

Or is there a way that I can make a fake authorization endpoint that just validates any token sent in the Auth header?

I would just like my integration tests to be able to work against the api, it would be great if it actually validated the token but if it can't I can fake it.

Thanks.

2 Answers

In usual fashion, right after I ask about something I've been stumped on for hours I figure it out. Here is the simple solution based on this other question which has a somewhat more convoluted answer (How can I set my IdentityServer4 BackChannelHandler from within an xUnit integration test using WebApplicationFactory?).

In your Startup.cs add a static property:

/// <summary>
/// For integrationtesting set this to Factory.Server.CreateHandler()
/// </summary>
public static HttpMessageHandler JwtBackChannelHandler { get; set; }

Then add this to your .AddIdentityServerAuthentication() options:

if (JwtBackChannelHandler != null)
{
    options.JwtBackChannelHandler = JwtBackChannelHandler;
}

In your constructor for your test class that implements IClassFixture<WebApplicationFactory<Startup>> add this:

Startup.JwtBackChannelHandler = Factory.Server.CreateHandler();

This will allow you to call the token endpoint and receive a real token and use that as an authentication header in your integration tests. Hot diggity.

This answer is for @liqSTAR on how to ignore IdentitiyServer 4 and faking the authorization in integration tests.

Create a class that looks like this:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(
        IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder, 
        ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var context = Context;
        List<Claim> claims;
        if (context.Request.Headers.Keys.Contains("my-name"))
        {
            var name = context.Request.Headers["my-name"].First();
            var id = context.Request.Headers.Keys.Contains("my-id") ? context.Request.Headers["my-id"].First() : "";
            claims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, name),
                    new Claim(ClaimTypes.NameIdentifier, id),
                };
        }
        else
        {
            claims = new List<Claim> { new Claim(ClaimTypes.Name, "Test user") };
        }
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        context.User = principal;

        var ticket = new AuthenticationTicket(principal, "Test");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

Then in your override void ConfigureWebHost(IWebHostBuilder builder) in your custom class that inherits from WebApplicationFactory<TStartup> add

services.AddAuthentication("Test")
  .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", options => {});

To the builder.ConfigureServices(services => { section.

Then in your test class add

Client.DefaultRequestHeaders.Add("my-id", "1234");
Client.DefaultRequestHeaders.Add("my-name", "Test_User");

to either your ctor or test methods when you want to dummy up a user so that the Authorize attribute works when your controller tries to do something with User. (Client is convenience property I have for Factory.CreateClient();)

The nice thing about this is that if you don't want an authorized user to be passed don't set the my-id and my-name headers.

I hope this helps get you going in the right direction.

Related