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

Integration testing with in-memory IdentityServer

我有一个 API 使用 IdentityServer4 进行令牌验证。 我想使用内存中的 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;
}

我认为您可能需要为您的授权中间件制作一个测试双重伪造,具体取决于您想要多少功能。所以基本上你想要一个中间件来完成授权中间件所做的一切减去对发现文档的反向通道调用。

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

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

看看 IdentityServer4.AccessTokenValidation 的肉长什么样 here. And follow up with a look at how JwtBearer Middleware looks here

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

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

IdentityServerSetup.cs class https://github.com/emedbo/identityserver-test-template/blob/master/tests/API.Tests/Config/IdentityServerSetup.cs 可以被抽象掉,例如Nu滚蛋,离开基地classIntegrationTestBase.cs

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

我建议阅读@james-fera 的post,而不是post 此处的代码,以了解基础知识,然后提取我的项目和运行 测试。

IdentityServer 是一个很棒的工具,如果能够使用 TestServer 框架,它会变得更好。

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

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 项目,我发现将 IdentityServerAuthenticationOptions 添加到 [=44= 中的服务集合中很有用] 使用 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 项目中的代码。

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

诀窍是使用配置为使用 IdentityServer4TestServer 创建处理程序。可以找到示例 here.

我为此创建了一个 nuget-package available to install and test using the Microsoft.AspNetCore.Mvc.Testing 库和最新版本的 IdentityServer4

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

None 的其他答案对我有用,因为它们依赖于 1) 一个静态字段来保存你的 HttpHandler 和 2) Startup class 来了解它可能会被测试处理程序。我发现以下方法有效,我认为它更干净。

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

我们不再尝试托管模拟 IdentityServer,而是按照其他人的建议使用 dummy/mock 授权方。

以下是我们的操作方法,以防有用:

创建了一个接受类型的函数,创建了一个测试身份验证中间件并使用 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;
    }

然后我们使用所需的角色创建我们所谓的 'Impersonators' (AuthenticationHandlers) 以模拟具有角色的用户(我们实际上将其用作基础 class,并创建派生的 class es以此为基础模拟不同的用户):

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