使用内存中的IdentityServer进行集成测试

29

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

我已经成功从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;
}
7个回答

29

你在初始问题中发布的代码是正确的。

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中间件直接与内存中的IdentityServer通信,而无需跳过任何麻烦。

另外,对于一个Api项目,我发现将IdentityServerAuthenticationOptions添加到 Startup.cs 中的服务集合中,使用TryAddSingleton而不是在行内创建,会更加实用:

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!我曾经为了让它工作而苦苦挣扎。我使用了稍微修改过的测试夹具,来自https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/testing,现在它运行得很好。 - JimiSweden
谢谢@JamesFera...这对我有用。稍微改了一下代码,我已经在另一个答案中发布供其他人使用。 - Rashmi Pandit
使用backchannelhandler - 真是太棒了! - BozoJoe
1
在2020年,.NET Core 3,也许你在这里是因为你需要这个链接,或者是因为你需要添加身份验证(AddAuthentication)。 - heringer
我真的愿意为一个完整的示例杀人,告诉我怎样做。我根本无法从你那里写的内容中弄清楚该怎么做。此外,看起来IdentityServer4有一些变化,因此需要修改一些东西(例如IdentityServerAuthenticationOptions处理程序)。 - Frank Hale
显示剩余2条评论

9

我知道需要一个比@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框架使其变得更加优秀。


如果你还有兴趣,我想看一下你提到的项目。我目前正在尝试做类似于你描述的事情。 - Thomas Jørgensen
你试过 @james-fera 上面发的吗?如果没有,我建议你先试试他的方法,因为我的解决方案需要更多的代码。 - Espen Medbø
我尝试过了,但是没有成功。但是后来我在我的IdentityServer设置中发现了一个配置错误。一旦修复,@james-fera的建议就完美地起作用了。 - Thomas Jørgensen
请查看我的更新答案。我一定会帮助你入门 :-) - Espen Medbø

4
我们放弃了尝试托管一个模拟IdentityServer,转而使用其他人建议的虚拟/模拟授权者。
以下是我们如何做到这一点的方法(如果有用的话):
创建一个函数,它接受一个类型,创建一个测试认证中间件,并使用ConfigureTestServices将其添加到DI引擎中(以便在调用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;
    }

接下来,我们创建了所谓的“模拟者”(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——拯救了我的理智。我结合了他的一些想法和这篇文章:https://visualstudiomagazine.com/blogs/tool-tracker/2019/11/mocking-authenticated-users.aspx - Bryan Lewis
3
代码无法编译:Impersonator 构造函数与 FreeUserImpersonator 类名不匹配,也不存在 base.claims。此外,它是抽象的,还有一个“无法为服务类型实例化…” 的消息。 - Cee McSharpface

4
我认为根据你需要多少功能,你可能需要为你的授权中间件制作一个测试替身。所以基本上你想要一个中间件,它可以执行授权中间件的所有操作,但不包括后端通道调用发现文档。
IdentityServer4.AccessTokenValidation是两个中间件的包装器。其中一个是JwtBearerAuthentication中间件,另一个是OAuth2IntrospectionAuthentication中间件。这两个中间件都通过http获取发现文档以用于令牌验证。如果您想进行内存自包含测试,这将成为问题。
如果您想自定义测试,您可能需要制作一个假版本的app.UseIdentityServerAuthentication,它不会执行获取发现文档的外部调用。它只填充HttpContext principal,以便可以测试您的[Authorize]策略。
查看IdentityServer4.AccessTokenValidation的核心内容,请点击此处。并继续查看JwtBearer Middleware的工作原理,请点击此处

非常感谢 @Lutando。你的第一个答案指引了我正确的方向。 - Espen Medbø
啊,好的@emedbo,我觉得制作一个假的测试双倍可能有点过了。但它能用 :) - Lutando
这种方法的问题在于,如果您希望您的持续集成系统运行集成测试,则需要将此身份验证欺骗集成到您的发布构建中。这可能是一个非常危险的情况。相比之下,拥有一个提供虚假身份验证以进行集成测试场景的实际IdentityServer服务似乎更加可取。 - Neutrino
@Neutrino,我有点困惑,如果我们使用WebApplicationFactory创建TestServer,那么我们不会将任何东西集成到我们的发布版本中,这不是事实吗? - Liam Fleming
@LiamFleming 当我第一次尝试这种方法时,我使用了一个身份验证欺骗中间件,该中间件在WebService本身中实现,并通过配置设置激活。这是有风险的,因为如果配置设置在实时系统上意外地(或其他方式)启用,则可能会发生问题。 - Neutrino
显示剩余2条评论

3

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

1
谢谢,这让我找到了正确的方向!在我的API项目中,将BackChannelHandler设置为TestServer.CreateHandler()的值,并将其传递给services.AddJwtBearer()中的JwtBearerOptions是关键。为了使身份验证通过,我还必须将IdentityServer TestServer的TestServer.BaseAddress设置为与JwtBearerOptions.Authority相同的URL。 - EM0

0

技巧是使用配置为使用 IdentityServer4TestServer 创建处理程序。 这里 可以找到示例。

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

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


这可能正是我正在寻找的东西。如果我添加了这个NuGet包,我是否正确地认为我应该能够在创建“TestServer”和“client”时将WebHostBuilder简单地替换为IdentityServerWebHostBuilder?您能展示一下它的样子或者指向一个示例吗? - Simon Lomax
嘿,你可以在这里找到示例: https://github.com/cleancodelabs/IdentityServer4.Contrib.AspNetCore.Testing/blob/master/test/IdentityServer4.Contrib.AspNetCore.Testing.Tests/IdentityServerProxyTests.cs希望它们能有所帮助! - alsami
谢谢。我从示例中可以看到如何创建内存中的IdentityServer4实例,但我想知道您是否能展示如何让我的API使用内存中的IdentityServer4实例而不是“真正”的实例来创建TestServer - Simon Lomax
我在我的一个宠物项目中有一个示例: https://github.com/alsami/etdb-userservice-aspnet-core/blob/master/test/Etdb.UserService.Bootstrap.Tests/Mocking/ApiServerStartup.cs#L65这个示例有点不同,因为我使用了 HttpClientFactory 来进行请求。已经有一个测试可以正常工作了。也许你想深入了解一下。 - alsami

0

其他答案对我都不起作用,因为它们依赖于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;


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接