【问题标题】:Building an integration test for an AspNetCore API that uses IdentityServer 4 for Auth为使用 IdentityServer 4 进行身份验证的 AspNetCore API 构建集成测试
【发布时间】:2019-04-09 10:41:28
【问题描述】:

我构建了一个简单的 AspNetCore 2.2 API,它使用 IdentityServer 4 来处理 OAuth。它工作正常,但我现在想添加集成测试,最近发现了this。我用它构建了一些运行良好的测试——只要我的控制器上没有[Authorize] 属性——但显然该属性需要存在。

我遇到了this stackoverflow question,从那里给出的答案中,我尝试将测试放在一起,但是当我尝试运行测试时仍然收到Unauthorized 响应。

请注意:我真的不知道在创建客户端时应该使用哪些细节。

  • 允许的范围应该是什么? (如果他们匹配真实 范围)

同样在构建IdentityServerWebHostBuilder

  • 我应该将什么传递给.AddApiResources? (也许是一个愚蠢的问题,但是 有没有关系)

如果有人可以指导我,将不胜感激。

这是我的测试:

[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);

}

我的创业班:

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();
    }
}

【问题讨论】:

  • 您能否添加用于实际令牌请求的代码?您收到的错误是什么?
  • @alsami 我得到一个“未经授权”,这是有道理的,因为我没有传递不记名令牌,所以我添加了一些我认为会这样做的代码,但现在会导致其他问题希望我已经在更新代码中的 cmets 中进行了解释。
  • @alsami 虽然我现在可以获得访问令牌,但我不知道如何将我的TestServerIdentityServerProxy 连接到 API
  • 能否在github上提供完整的源代码?我需要手动测试看看有什么不工作。

标签: c# asp.net-core integration-testing identityserver4


【解决方案1】:

编辑:

以下建议是一个问题。由于尝试构建 WebHostBuilder twice 的异常,原始源代码失败。其次,配置文件只存在于 API 项目中,而不存在于测试项目中,这就是为什么没有设置权限的原因。

而不是这样做

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

你必须这样做:

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

您可以找到示例here

希望对我有帮助!

【讨论】:

  • 感谢您的建议,但我似乎也无法正常工作。我已按您的要求添加了git repo。这是一个超级简单的 API,使用 IdentityServer 进行身份验证。该项目还有一个 IntegrationTest 项目,其中包括我在此处发布的测试。如果您能看一下并告诉我哪里出错了,我将不胜感激。
  • 给你一个修复它的合并请求:github.com/simax/SuperSimpleAPI/pull/1
  • 太棒了。非常感谢你的帮助。一件事-在许多:)中让我感到困惑的是,我没有意识到我需要从identityServerProxy.IdentityServer 打电话给CreateHandler() 我想我是直接在identityServerProxy 上寻找它。再次感谢您创建 nuget 包并提供帮助 - 非常感谢。
  • @alsami 我已经尝试过你的修复,github.com/simax/SuperSimpleAPI 并且我不断收到 http 302。我已经按照你的描述申请了。知道为什么吗?
  • @Ktt 很高兴它成功了!是的,我也在考虑这个问题。另一个示例可以在这里找到 btw:github.com/cleancodelabs/…
【解决方案2】:

不影响生产代码的解决方案:

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);
                });
            });
    }
}

它的用法是:

 _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/")}
        };

TestJwtBearerOptions 只是将请求代理到 identityServerClient。您可以在此处找到实现: https://gist.github.com/ru-sh/048e155d73263912297f1de1539a2687

【讨论】:

  • 链接失效:你有TestJwtBearerOptions的代码吗?
  • 不,我不小心把它删了。但是实现起来应该很简单。您将需要继承 JwtBearerOptions 并将断点放入其方法中以确定哪​​个处理 HTTP 消息。 AFAIR,它是 BackchannelHttpHandler。
【解决方案3】:

如果您不想依赖静态变量来保存 HttpHandler,我发现以下方法可以工作。我认为它更干净。

首先创建一个可以在创建 TestHost 之前实例化的对象。这是因为在创建 TestHost 之前您不会拥有 HttpHandler,因此您需要使用包装器。

    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; }
    }

然后

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;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-11-22
    • 1970-01-01
    • 2020-08-04
    • 2013-03-18
    • 2021-05-11
    • 2019-09-27
    • 1970-01-01
    相关资源
    最近更新 更多