27

我有一个使用 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;
}
4

7 回答 7

27

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

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

于 2016-11-12T03:50:32.483 回答
8

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

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

https://github.com/emedbo/identityserver-test-template/blob/master/tests/API.Tests/Config/IdentityServerSetup.cs可以被抽象掉,例如 NuGetted,留下基IdentityServerSetup.csIntegrationTestBase.cs

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

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

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

于 2016-09-09T10:44:37.040 回答
4

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

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

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

在此处查看 IdentityServer4.AccessTokenValidation 的内容。并跟进看看 JwtBearer Middleware在这里的样子

于 2016-09-08T13:18:27.177 回答
4

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

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

创建了一个采用类型的函数,创建了一个测试身份验证中间件,并使用配置测试服务将其添加到 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)的东西,它具有所需的角色来模仿具有角色的用户(我们实际上将它用作基类,并基于它创建派生类来模拟不同的用户):

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);
于 2020-01-08T23:52:43.007 回答
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>();
    }
于 2017-11-09T03:10:27.373 回答
0

诀窍是使用TestServer配置为使用的IdentityServer4. 样品可以在这里找到。

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

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

于 2019-04-08T12:36:55.657 回答
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;

于 2019-10-01T19:01:41.150 回答