29

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;
}
Pang
  • 9,564
  • 146
  • 81
  • 122
Espen Medbø
  • 2,305
  • 1
  • 19
  • 24

7 Answers7

29

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.

Pang
  • 9,564
  • 146
  • 81
  • 122
James Fera
  • 291
  • 4
  • 6
  • Thanks @JamesFera! I was tearing my hair trying to get this working. I use a slightly modyfied TestFixture from https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/testing and it works great. – JimiSweden Jan 17 '17 at 14:48
  • Thanks @JamesFera ... this worked for me. Slight code changes, which I have posted in a separate answer for others to use. – Rashmi Pandit Nov 09 '17 at 03:05
  • using the backchannelhandler - that was gold! – BozoJoe Nov 13 '19 at 20:11
  • I could not make it work, I get http 302 redirect, any idea? – Ktt Jan 21 '20 at 10:47
  • 1
    In 2020, .NET Core 3, maybe you are here because you need [this link](https://github.com/IdentityServer/IdentityServer4/issues/3666) or because you need AddAuthentication. – heringer Mar 31 '20 at 20:25
  • I would literally kill for a full example on how to do this. I simply cannot figure out how to do it from what you wrote there. Plus looks like IdentityServer4 has some changes so something things need to be modified (eg the IdentityServerAuthenticationOptions handlers) – Frank Hale Apr 30 '20 at 02:15
  • @heringer unfortunately there is not enough information in that post to figure it out. I mean I tried but with no clear example I'm just shooting in the wind. – Frank Hale Apr 30 '20 at 02:16
9

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.

Espen Medbø
  • 2,305
  • 1
  • 19
  • 24
4

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

Lutando
  • 4,909
  • 23
  • 42
  • Thanks a lot @Lutando. Your first answer pointed me in the right direction. – Espen Medbø Sep 09 '16 at 10:45
  • ah ok @emedbo I thought it may be a bit much to make the fake test double. But it works :) – Lutando Sep 09 '16 at 11:32
  • The problem with this apprach is that if you want your continuous integration system to run your integration tests, that requires integrating this authentication spoofing into your release build. That is potentially a really dangerous situation. The alternative of having an actual IdentityServer service that provides dummy authentication for the integration test scenario seems massively preferable. – Neutrino Sep 04 '19 at 09:27
  • @Neutrino, I'm a little confused, if we use the WebApplicationFactory to create the TestServer, then we're not integrating anything into our release build, isn't that the case? – Liam Fleming Jan 02 '20 at 01:45
  • @LiamFleming When I first tried this approach I used an authentication spoofing middleware that was implemented in the webservice itself and activated by a configuration setting. This was risky as if the configuration setting could accidentally (or otherwise) became enabled on the live system. – Neutrino Jan 08 '20 at 16:54
  • We refactored this so that the spoofing middleware was defined in the test assembly itself and applied by overriding IServiceCollection.AddAuthentication in the test assembly. Since the test assembly is not deployed in the release build there was no way this could be enabled accidentally. – Neutrino Jan 08 '20 at 16:55
  • @Neutrino Makes sense, I solved it with something similar, thanks for explaining! – Liam Fleming Jan 08 '20 at 23:38
4

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);
Pang
  • 9,564
  • 146
  • 81
  • 122
Liam Fleming
  • 1,016
  • 12
  • 17
  • While I ended up implementing mine slightly different, Liam's idea to move away from the complexities of mocking the identity server to using AuthHandlers saved my sanity. I used some of his ideas in conjunction with this article: https://visualstudiomagazine.com/blogs/tool-tracker/2019/11/mocking-authenticated-users.aspx – Bryan Lewis Mar 17 '20 at 17:52
  • 2
    the code does not compile: the `Impersonator` constructor does not match the `FreeUserImpersonator` class name, and there is no `base.claims`. Also it is abstract and there is a "Cannot instantiate ... for service type" message. – Cee McSharpface Aug 28 '21 at 11:13
3

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>();
    }
Pang
  • 9,564
  • 146
  • 81
  • 122
Rashmi Pandit
  • 23,230
  • 17
  • 71
  • 111
  • 1
    Thanks, this got me on the right track! Setting `BackChannelHandler` on the `JwtBearerOptions` passed to `services.AddJwtBearer()` in my API project to value of `TestServer.CreateHandler()` for the IdentityServer TestServer was key for me. For authentication to pass I also had to set `TestServer.BaseAddress` for the IdentityServer TestServer to the same URL as `JwtBearerOptions.Authority` – EM0 May 08 '19 at 15:41
0

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.

alsami
  • 8,996
  • 3
  • 25
  • 36
  • This might be just what I'm looking for. Am I correct in thinking that if I add this nuget package I should be able to simply replace WebHostBuilder with IdentityServerWebHostBuilder when creating a `TestServer` and `client`. Could you show how that would look or point me to a sample please. – Simon Lomax Apr 08 '19 at 13:05
  • Hey, you find samples here: https://github.com/cleancodelabs/IdentityServer4.Contrib.AspNetCore.Testing/blob/master/test/IdentityServer4.Contrib.AspNetCore.Testing.Tests/IdentityServerProxyTests.cs I hope they help! – alsami Apr 08 '19 at 13:28
  • Thanks. I can see from the samples how to create in memory `IdentityServer4` instances but I wonder if you could show how to make the `TestServer` for my api use the in memory `IdentityServer4` instance instead of the 'real' one. – Simon Lomax Apr 09 '19 at 08:43
  • I have sample here on one of my pet-projects: https://github.com/alsami/etdb-userservice-aspnet-core/blob/master/test/Etdb.UserService.Bootstrap.Tests/Mocking/ApiServerStartup.cs#L65 It's a little bit different since I am using `HttpClientFactory` to do the requests. There is one tests already working like that. Maybe you will want to dig into it a little bit. – alsami Apr 09 '19 at 08:49
0

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;

Pang
  • 9,564
  • 146
  • 81
  • 122
Josh Mouch
  • 3,480
  • 1
  • 37
  • 34