我有一个使用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;
}
答案 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.cs
类https://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;