【问题标题】:Integration testing with in-memory IdentityServer使用内存中的 IdentityServer 进行集成测试
【发布时间】:2017-01-16 08:25:37
【问题描述】:

我有一个使用 IdentityServer4 进行令牌验证的 API。 我想用内存中的 TestServer 对这个 API 进行单元测试。我想将 IdentityServer 托管在内存中的 TestServer 中。

我已经设法从 IdentityServer 创建了一个令牌。

这是我已经走了多远,但我收到错误“无法从 http://localhost:54100/.well-known/openid-configuration 获取配置”

Api 使用具有不同策略的 [Authorize] 属性。这就是我要测试的。

这可以做到吗,我做错了什么? 我曾尝试查看 IdentityServer4 的源代码,但没有遇到类似的集成测试场景。

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

在我的单元测试中:

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

【问题讨论】:

    标签: identityserver4


    【解决方案1】:

    我认为您可能需要根据您想要多少功能为您的授权中间件进行测试双重伪造。所以基本上你需要一个中间件来完成授权中间件所做的所有事情,减去对发现文档的反向通道调用。

    IdentityServer4.AccessTokenValidation 是两个中间件的包装器。 JwtBearerAuthentication 中间件和 OAuth2IntrospectionAuthentication 中间件。这两者都通过 http 抓取发现文档以用于令牌验证。如果您想进行内存中独立测试,这是一个问题。

    如果你想解决这个问题,你可能需要制作一个伪造的app.UseIdentityServerAuthentication 版本,它不会执行获取发现文档的外部调用。它仅填充 HttpContext 主体,以便可以测试您的 [Authorize] 策略。

    查看 IdentityServer4.AccessTokenValidation 的内容here。并跟进看看 JwtBearer Middleware 的样子here

    【讨论】:

    • 非常感谢@Lutando。您的第一个回答为我指明了正确的方向。
    • 啊好的@emedbo 我认为让假测试加倍可能有点过分。但它有效:)
    • 这种方法的问题在于,如果您希望持续集成系统运行集成测试,则需要将此身份验证欺骗集成到您的发布版本中。这可能是一个非常危险的情况。拥有一个为集成测试场景提供虚拟身份验证的实际 IdentityServer 服务的替代方案似乎非常可取。
    • @Neutrino,我有点困惑,如果我们使用 WebApplicationFactory 来创建 TestServer,那么我们不会将任何东西集成到我们的发布版本中,不是吗?
    • @LiamFleming 当我第一次尝试这种方法时,我使用了一个身份验证欺骗中间件,它在 web 服务本身中实现并由配置设置激活。这是有风险的,好像配置设置可能会意外(或以其他方式)在实时系统上启用。
    【解决方案2】:

    我知道需要比@james-fera 发布的更完整的答案。我从他的回答中了解到,做了一个由测试项目和 API 项目组成的 github 项目。代码应该是不言自明的并且不难理解。

    https://github.com/emedbo/identityserver-test-template

    IdentityServerSetup.cshttps://github.com/emedbo/identityserver-test-template/blob/master/tests/API.Tests/Config/IdentityServerSetup.cs 可以抽象出来,例如NuGet了,离开基类IntegrationTestBase.cs

    本质是可以让测试IdentityServer像普通的IdentityServer一样工作,有用户、客户端、范围、密码等。我做了DELETE方法[Authorize(Role="admin)]来证明这一点。

    我建议不要在此处发布代码,而是阅读 @james-fera 的帖子以了解基础知识,然后拉取我的项目并运行测试。

    IdentityServer 是一个非常棒的工具,并且能够使用 TestServer 框架,它变得更好。

    【讨论】:

    • 如果你还在努力,想看看你提到的项目。我目前正在尝试做与您描述的类似的事情。
    • 你试过上面@james-fera 发布的内容吗?如果没有,我会先尝试,因为我的解决方案需要更多代码。
    • 我试过了,还是不行。但后来我在身份服务器设置中发现了一个配置错误。修复后,@james-fera 的建议非常有效。
    • 请看我更新的答案。我一定会帮助你开始:-)
    【解决方案3】:

    您在最初的问题中发布的代码是正确的。

    IdentityServerAuthenticationOptions 对象具有覆盖用于反向通道通信的默认 HttpMessageHandlers 的属性。

    将其与 TestServer 对象上的 CreateHandler() 方法结合后,您将获得:

    //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
    

    这允许您的 Api 项目中的 AccessTokenValidation 中间件直接与您的 In-Memory IdentityServer 进行通信,而无需费劲。

    附带说明,对于 Api 项目,我发现使用 TryAddSingletonIdentityServerAuthenticationOptions 添加到 Startup.cs 中的服务集合很有用> 而不是内联创建它:

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

    这允许您在测试中注册 IdentityServerAuthenticationOptions 对象,而无需更改 Api 项目中的代码。

    【讨论】:

    • 感谢@JamesFera!我正在撕裂我的头发试图让这个工作。我使用了来自docs.microsoft.com/en-us/aspnet/core/mvc/controllers/testing 的稍微修改过的TestFixture,效果很好。
    • 谢谢@JamesFera ...这对我有用。轻微的代码更改,我已在单独的答案中发布以供其他人使用。
    • 使用 backchannelhandler - 那是黄金!
    • 我无法让它工作,我得到了 http 302 重定向,知道吗?
    • 2020 年,.NET Core 3,也许你在这里是因为你需要this link 或者因为你需要 AddAuthentication。
    【解决方案4】:

    测试 API 启动:

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

    在我的单元测试项目中将 AuthServer.Handler 分配给 TestApi BackChannelHandler:

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

    【讨论】:

    • 谢谢,这让我走上了正轨!在我的 API 项目中将 JwtBearerOptions 上的 BackChannelHandler 设置为 services.AddJwtBearer() 的值 TestServer.CreateHandler() 对我来说很关键。为了通过身份验证,我还必须将 IdentityServer TestServer 的 TestServer.BaseAddress 设置为与 JwtBearerOptions.Authority 相同的 URL
    【解决方案5】:

    诀窍是使用配置为使用IdentityServer4TestServer 创建一个处理程序。样品可以在here找到。

    为此,我创建了一个nuget-package 可用于使用Microsoft.AspNetCore.Mvc.Testing 库和最新版本的IdentityServer4 进行安装和测试。

    它封装了构建适当的WebHostBuilder 所需的所有基础结构代码,然后通过为内部使用的HttpClient 生成HttpMessageHandler 来创建TestServer

    【讨论】:

    • 这可能正是我想要的。我是否认为如果我添加这个 nuget 包,我应该能够在创建TestServerclient 时简单地将 WebHostBuilder 替换为 IdentityServerWebHostBuilder。您能否展示一下它的外观或指向我的示例。
    • 嘿,你可以在这里找到样本:github.com/cleancodelabs/…我希望他们有所帮助!
    • 谢谢。我可以从示例中看到如何在内存中创建 IdentityServer4 实例,但我想知道您是否可以展示如何使我的 api 的 TestServer 使用内存中的 IdentityServer4 实例而不是“真实”实例。跨度>
    • 我有一个关于我的宠物项目的样本:github.com/alsami/etdb-userservice-aspnet-core/blob/master/test/… 这有点不同,因为我使用HttpClientFactory 来执行请求。已经有一个测试可以这样工作。也许你会想深入研究一下。
    【解决方案6】:

    没有其他答案对我有用,因为它们依赖于 1) 一个静态字段来保存您的 HttpHandler 和 2) Startup 类知道它可能会被赋予一个测试处理程序。我发现以下方法可以工作,我认为它更干净。

    首先创建一个可以在创建 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;
    
    

    【讨论】:

      【解决方案7】:

      我们不再尝试托管模拟 IdentityServer,而是按照此处其他人的建议使用了虚拟/模拟授权器。

      如果有用,我们会这样做:

      创建了一个函数,该函数接受一个类型,创建一个测试身份验证中间件,并使用 ConfigureTestServices 将其添加到 DI 引擎(以便在调用启动。)

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

      然后我们创建我们所谓的“Impersonators”(AuthenticationHandlers),并使用所需的角色来模仿具有角色的用户(我们实际上将其用作基类,并基于此创建派生类来模拟不同的用户):

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

      最后,我们可以执行如下集成测试:

      // Arrange
      HttpClient client = GetImpersonatedClient<FreeUserImpersonator>();
      
      // Act
      var response = await client.GetAsync("api/things");
      
      // Assert
      Assert.That.IsSuccessful(response);
      

      【讨论】:

      • 虽然我最终实现了我的略有不同,但 Liam 将复杂性从模拟身份服务器转移到使用 AuthHandlers 的想法挽救了我的理智。我结合这篇文章使用了他的一些想法:visualstudiomagazine.com/blogs/tool-tracker/2019/11/…
      • 代码无法编译:Impersonator构造函数与FreeUserImpersonator类名不匹配,也没有base.claims。它也是抽象的,并且有一个“无法为服务类型实例化...”消息。
      猜你喜欢
      • 1970-01-01
      • 2019-06-30
      • 1970-01-01
      • 2013-11-22
      • 1970-01-01
      • 1970-01-01
      • 2019-11-27
      • 2015-03-21
      • 1970-01-01
      相关资源
      最近更新 更多