web-dev-qa-db-fra.com

Test d'intégration avec IdentityServer en mémoire

J'ai une API qui utilise IdentityServer4 pour la validation des jetons. Je veux tester unitaire cette API avec un TestServer en mémoire. Je voudrais héberger le serveur d'identité dans le serveur de test en mémoire.

J'ai réussi à créer un jeton à partir du serveur d'identité.

C'est le chemin parcouru, mais j'obtiens une erreur "Impossible d'obtenir la configuration de http: // localhost: 54100/.well-known/openid-configuration "

L'API utilise l'attribut [Authorize] avec différentes politiques. Voilà ce que je veux tester.

Cela peut-il être fait et que fais-je de mal? J'ai essayé de regarder le code source d'IdentityServer4, mais je n'ai pas rencontré de scénario de test d'intégration similaire.

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

Et dans mon test unitaire:

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;
}
22
Espen Medbø

Je pense que vous devrez probablement faire un double test de faux pour votre middleware d'autorisation en fonction de la quantité de fonctionnalités que vous souhaitez. Donc, fondamentalement, vous voulez un middleware qui fait tout ce que le middleware d'autorisation fait moins l'appel de canal arrière vers le document de découverte.

IdentityServer4.AccessTokenValidation est un wrapper autour de deux middlewares. Le middleware JwtBearerAuthentication et le OAuth2IntrospectionAuthentication middleware. Les deux récupèrent le document de découverte sur http pour l'utiliser pour la validation des jetons. Ce qui pose problème si vous souhaitez effectuer un test autonome en mémoire.

Si vous voulez passer par le problème, vous devrez probablement créer une fausse version de app.UseIdentityServerAuthentication qui ne fait pas l'appel externe qui récupère le document de découverte. Il remplit uniquement le principal HttpContext afin que vos politiques [Autoriser] puissent être testées.

Découvrez à quoi ressemble la viande d'IdentityServer4.AccessTokenValidation ici . Et continuez avec un aperçu de l'apparence du middleware JwtBearer ici

3
Lutando

Vous étiez sur la bonne voie avec le code affiché dans votre question initiale.

L'objet IdentityServerAuthenticationOptions a des propriétés pour remplacer la valeur par défaut HttpMessageHandlers qu'il utilise pour la communication de canal arrière.

Une fois que vous combinez cela avec la méthode CreateHandler () sur votre objet TestServer, vous obtenez:

    //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

Cela permet au middleware AccessTokenValidation de votre projet Api de communiquer directement avec votre In-Memory IdentityServer sans avoir à passer par des cercles.

En remarque, pour un projet Api, je trouve utile d'ajouter IdentityServerAuthenticationOptions à la collection de services dans Startup.cs en utilisant TryAddSingleton au lieu de la création en ligne:

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

        //...

    }

Cela vous permet d'enregistrer l'objet IdentityServerAuthenticationOptions dans vos tests sans avoir à modifier le code dans le projet Api.

22
James Fera

Je comprends qu'il est nécessaire d'avoir une réponse plus complète que ce que @ james-fera a publié. J'ai appris de sa réponse et réalisé un projet github composé d'un projet de test et d'un projet API. Le code doit être explicite et pas difficile à comprendre.

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

Le IdentityServerSetup.cs classe https://github.com/emedbo/identityserver-test-template/blob/master/tests/API.Tests/Config/IdentityServerSetup.cs peut être résumé par exemple NuGetted away, laissant la classe de base IntegrationTestBase.cs

L'essentiel est que le test IdentityServer puisse fonctionner comme un IdentityServer normal, avec des utilisateurs, des clients, des étendues, des mots de passe, etc. J'ai fait la méthode DELETE [Authorize (Role = "admin)] pour le prouver.

Au lieu de publier du code ici, je recommande de lire le post de @ james-fera pour obtenir les bases puis tirer mon projet et exécuter des tests.

IdentityServer est un excellent outil, et avec la possibilité d'utiliser le framework TestServer, il devient encore meilleur.

6
Espen Medbø

Tester le démarrage de l'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,
            ...
        });

        ...
    }
}

Affectation de AuthServer.Handler à TestApi BackChannelHandler dans mon projet de test unitaire:

    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>();
    }
2
Rashmi Pandit

Nous avons évité d'essayer d'héberger un serveur d'identité fictif et avons utilisé des autoriseurs factices/fictifs comme suggéré par d'autres ici.

Voici comment nous l'avons fait au cas où cela serait utile:

A créé une fonction qui prend un type, crée un middleware d'authentification de test et l'ajoute au moteur DI à l'aide de Configure Test Services (pour qu'il s'appelle après l'appel au démarrage.)

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

Ensuite, nous créons ce que nous avons appelé des `` imitateurs '' (AuthenticationHandlers) avec les rôles souhaités pour imiter les utilisateurs avec des rôles (nous l'avons en fait utilisé comme classe de base et créer des classes dérivées basées sur cela pour simuler différents utilisateurs):

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

Enfin, nous pouvons effectuer nos tests d'intégration comme suit:

// Arrange
HttpClient client = GetImpersonatedClient<FreeUserImpersonator>();

// Act
var response = await client.GetAsync("api/things");

// Assert
Assert.That.IsSuccessful(response);

Tout commentaire serait le bienvenu :)

1
Liam Fleming

L'astuce consiste à créer un gestionnaire à l'aide de TestServer qui est configuré pour utiliser IdentityServer4. Des échantillons peuvent être trouvés ici .

J'ai créé une bibliothèque nuget-package disponible pour l'installation et le test à l'aide de la bibliothèque Microsoft.AspNetCore.Mvc.Testing et de la dernière version de IdentityServer4 dans ce but.

Il encapsule tout le code d'infrastructure nécessaire pour construire un WebHostBuilder approprié qui est ensuite utilisé pour créer un TestServer en générant le HttpMessageHandler pour le HttpClient utilisé en interne.

0
alsami

Aucune des autres réponses n'a fonctionné pour moi car elles reposent sur 1) un champ statique pour contenir votre HttpHandler et 2) la classe Startup pour savoir que l'on peut lui attribuer un gestionnaire de test. J'ai trouvé ce qui suit fonctionner, ce qui, je pense, est beaucoup plus propre.

Créez d'abord un objet que vous pouvez instancier avant la création de votre TestHost. En effet, vous n'aurez pas le HttpHandler avant la création du TestHost, vous devez donc utiliser un wrapper.

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

Ensuite

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;

0
Josh Mouch