トークンの検証にIdentityServer4を使用するAPIがあります。このAPIをメモリ内TestServerで単体テストしたいのですが。 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;
}
おそらく、必要な機能に応じて、承認ミドルウェアのテストを二重に偽造する必要があると思います。したがって、基本的には、承認ミドルウェアが実行するすべてのことを行い、ディスカバリドキュメントへのバックチャネル呼び出しを行わないミドルウェアが必要です。
IdentityServer4.AccessTokenValidationは、2つのミドルウェアのラッパーです。 JwtBearerAuthentication
ミドルウェア、およびOAuth2IntrospectionAuthentication
ミドルウェア。これらはどちらも、トークンの検証に使用するためにhttp経由でディスカバリドキュメントを取得します。これは、メモリ内自己完結型テストを実行する場合に問題になります。
問題を解決したい場合は、おそらく、発見ドキュメントを取得する外部呼び出しを行わないapp.UseIdentityServerAuthentication
の偽バージョンを作成する必要があります。 [Authorize]ポリシーをテストできるように、HttpContextプリンシパルのみを入力します。
IdentityServer4.AccessTokenValidationの中身がどのように表示されるかを確認してください here 。そして、JwtBearerミドルウェアがどのように見えるかを確認してください ここ
あなたは最初の質問に投稿されたコードで正しい軌道に乗っていました。
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);
//...
}
これにより、Apiプロジェクトのコードを変更せずに、テストにIdentityServerAuthenticationOptionsオブジェクトを登録できます。
@ 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 = "admin)]を作成しました。
ここにコードを投稿する代わりに、@ james-feraの投稿を読んで基本を取得し、プロジェクトをプルしてテストを実行することをお勧めします。
IdentityServerは非常に優れたツールであり、TestServerフレームワークを使用できるため、さらに優れています。
テスト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,
...
});
...
}
}
ユニットテストプロジェクトのTestApi BackChannelHandlerにAuthServer.Handlerを割り当てます。
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>();
}
ここでは、モックのIdentityServerをホストするのをやめ、ダミー/モックのオーソライザーを使用しました。
役に立つ場合に備えて、次のようにしました。
タイプを受け取り、テスト認証ミドルウェアを作成し、Configure Test Servicesを使用してそれをDIエンジンに追加する関数を作成しました(そのため、呼び出されますafterスタートアップへの呼び出し。)
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);
どんなフィードバックでも大歓迎です:)
トリックは、IdentityServer4
を使用するように構成されたTestServer
を使用してハンドラーを作成することです。サンプルが見つかります ここ 。
Microsoft.AspNetCore.Mvc.Testing ライブラリとこの目的のためのIdentityServer4
の最新バージョンを使用して、インストールとテストに使用できる nuget-package を作成しました。
これは、内部で使用されるWebHostBuilder
のTestServer
を生成することによりHttpMessageHandler
を作成するために使用される適切なHttpClient
を構築するために必要なすべてのインフラストラクチャコードをカプセル化します。
他の回答はどれも私にとってうまくいきませんでした。なぜなら、1)HttpHandlerを保持する静的フィールドと、2)スタートアップクラスがテストハンドラーが与えられる可能性があるという知識を持つことに依存しているからです。私は次のものが機能することを発見しました。
まず、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;