使用内存IdentityServer进行集成测试

时间:2016-09-08 12:00:15

标签: identityserver4

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

我已设法从IdentityServer创建令牌。

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

Api使用[授权] - 属性与不同的策略。这是我想测试的。

这可以做到,我做错了什么? 我试图查看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 个答案:

答案 0 :(得分:18)

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

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项目,我发现使用 TryAddSingleton IdentityServerAuthenticationOptions 添加到 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项目中的代码。

答案 1 :(得分:5)

我知道需要一个比@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,例如NuGetted离开基类IntegrationTestBase.cs

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

不是在这里发布代码,我建议阅读@james-fera的帖子来获取基础知识然后拉我的项目并运行测试。

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

答案 2 :(得分:3)

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

IdentityServer4.AccessTokenValidation是两个中间件的包装器。 JwtBearerAuthentication中间件和OAuth2IntrospectionAuthentication中间件。这两个都通过http获取发现文档以用于令牌验证。如果要进行内存中自包含测试,则会出现问题。

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

查看IdentityServer4.AccessTokenValidation的内容如何here。接下来看看JwtBearer Middleware的外观here

答案 3 :(得分:2)

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

答案 4 :(得分:1)

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

如果有用的话,这是我们要做的:

创建了一个带有类型的函数,创建了一个测试身份验证中间件,并使用Configure Test Services将其添加到DI引擎中(这样,在调用之后启动。)

If (Not Not MyArray) <> 0 Then 'Means it is allocated

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

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

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

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

答案 5 :(得分:0)

技巧是使用配置为使用TestServer的{​​{1}}创建处理程序。可以找到示例here

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

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

答案 6 :(得分: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;