Building an integration test for an AspNetCore API that uses IdentityServer 4 for Auth

橙三吉。 提交于 2019-12-11 05:49:53

问题


I've built a simple AspNetCore 2.2 API that uses IdentityServer 4 to handle OAuth. It's working fine but I'd now like to add integration tests and recently discovered this. I used it to build some tests which all worked fine - as long as I didn't have the [Authorize] attribute on my controllers - but obviously that attribute needs to be there.

I came across this stackoverflow question and from the answers given there I tried to put a test together but I'm still getting an Unauthorized response when I try to run tests.

Please note: I really don't know what details I should be using when I'm creating the client.

  • What should the allowed scopes be? (Should they match the real scopes)

Also when building the IdentityServerWebHostBuilder

  • What should I pass to .AddApiResources? (Maybe a dumb question but does it matter)

If anyone can guide me it would be greatly appreciated.

Here is my test:

[Fact]
public async Task Attempt_To_Test_InMemory_IdentityServer()
{
    // Create a client
        var clientConfiguration = new ClientConfiguration("MyClient", "MySecret");

        var client = new Client
        {
            ClientId = clientConfiguration.Id,
            ClientSecrets = new List<Secret>
            {
                new Secret(clientConfiguration.Secret.Sha256())
            },
            AllowedScopes = new[] { "api1" },
            AllowedGrantTypes = new[] { GrantType.ClientCredentials },
            AccessTokenType = AccessTokenType.Jwt,
            AllowOfflineAccess = true
        };

        var webHostBuilder = new IdentityServerWebHostBuilder()
            .AddClients(client)
            .AddApiResources(new ApiResource("api1", "api1name"))
            .CreateWebHostBuilder();

        var identityServerProxy = new IdentityServerProxy(webHostBuilder);
        var tokenResponse = await identityServerProxy.GetClientAccessTokenAsync(clientConfiguration, "api1");

        // *****
        // Note: creating an IdentityServerProxy above in order to get an access token
        // causes the next line to throw an exception stating: WebHostBuilder allows creation only of a single instance of WebHost
        // *****

        // Create an auth server from the IdentityServerWebHostBuilder 
        HttpMessageHandler handler;
        try
        {
            var fakeAuthServer = new TestServer(webHostBuilder);
            handler = fakeAuthServer.CreateHandler();
        }
        catch (Exception e)
        {
            throw;
        }

        // Create an auth server from the IdentityServerWebHostBuilder 
        HttpMessageHandler handler;
        try
        {
            var fakeAuthServer = new TestServer(webHostBuilder);
            handler = fakeAuthServer.CreateHandler();
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }

        // Set the BackChannelHandler of the 'production' IdentityServer to use the 
        // handler form the fakeAuthServer
        Startup.BackChannelHandler = handler;
        // Create the apiServer
        var apiServer = new TestServer(new WebHostBuilder().UseStartup<Startup>());
        var apiClient = apiServer.CreateClient();


        apiClient.SetBearerToken(tokenResponse.AccessToken);

        var user = new User
        {
            Username = "simonlomax@ekm.com",
            Password = "Password-123"
        };

        var req = new HttpRequestMessage(new HttpMethod("GET"), "/api/users/login")
        {
            Content = new StringContent(JsonConvert.SerializeObject(user), Encoding.UTF8, "application/json"),
        };

        // Act
        var response = await apiClient.SendAsync(req);

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

}

My Startup class:

public class Startup
{

    public IConfiguration Configuration { get; }
    public static HttpMessageHandler BackChannelHandler { get; set; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        ConfigureAuth(services);    
        services.AddTransient<IPassportService, PassportService>();
        services.Configure<ApiBehaviorOptions>(options =>
        {
            options.SuppressModelStateInvalidFilter = true;
        });

    }

    protected virtual void ConfigureAuth(IServiceCollection services)
    {
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
                options.Audience = Configuration.GetValue<string>("IdentityServerAudience");
                options.BackchannelHttpHandler = BackChannelHandler;
            });
    }


    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }

        app.UseAuthentication();
        app.UseHttpsRedirection();
        app.UseMvc();
        app.UseExceptionMiddleware();
    }
}

回答1:


Edit:

The below suggestion was one problem. The original source-code failed due to an exception by trying to build WebHostBuilder twice. Secondly the configuration-file was only present in the API project, not in the test-project, thats why authority wasn't set as well.

Instead of doing this

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
   .AddJwtBearer(options =>
   {
       options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
       options.Audience = Configuration.GetValue<string>("IdentityServerAudience");
       options.BackchannelHttpHandler = BackChannelHandler;
   });

You have to do something like this:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
   .AddIdentityServerAuthentication(options =>
   {
      options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
      options.JwtBackChannelHandler = BackChannelHandler;
    });

You can find a sample here.

Hope that helps, worked for me!




回答2:


If you don't want to rely on a static variable to hold the HttpHandler, I've found the following to work. I think it's 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;




回答3:


A solution which doesn't affect production code:

public class TestApiWebApplicationFactory<TStartup>
    : WebApplicationFactory<TStartup> where TStartup : class
{
    private readonly HttpClient _identityServerClient;

    public TestApiWebApplicationFactory(HttpClient identityServerClient)
    {
        _identityServerClient = identityServerClient;
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        base.ConfigureWebHost(builder);

        builder.ConfigureServices(
            s =>
            {
                s.AddSingleton<IConfigureOptions<JwtBearerOptions>>(services =>
                {
                    return new TestJwtBearerOptions(_identityServerClient);
                });
            });
    }
}

and its usage is:

 _factory = new WebApplicationFactory<Startup>()
        {
            ClientOptions = {BaseAddress = new Uri("http://localhost:5000/")}
        };

        _apiFactory = new TestApiWebApplicationFactory<SampleApi.Startup>(_factory.CreateClient())
        {
            ClientOptions = {BaseAddress = new Uri("http://localhost:5001/")}
        };

The TestJwtBearerOptions just proxies requests to identityServerClient. The implementation you can find here: https://gist.github.com/ru-sh/048e155d73263912297f1de1539a2687



来源:https://stackoverflow.com/questions/55590928/building-an-integration-test-for-an-aspnetcore-api-that-uses-identityserver-4-fo

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!