1
votes

Création d'un test d'intégration pour une API AspNetCore qui utilise IdentityServer 4 pour Auth

J'ai construit une API AspNetCore 2.2 simple qui utilise IdentityServer 4 pour gérer OAuth. Cela fonctionne bien, mais j'aimerais maintenant ajouter des tests d'intégration et découvrir récemment this . Je l'ai utilisé pour construire des tests qui ont tous bien fonctionné - tant que je n'avais pas l'attribut [Authorize] sur mes contrôleurs - mais évidemment cet attribut doit être là.

Je suis venu à travers cette question stackoverflow et à partir des réponses qui y sont données, j'ai essayé de mettre un test ensemble, mais j'obtiens toujours une réponse Non autorisé lorsque j'essaie d'exécuter des tests.

Remarque : je ne sais vraiment pas quoi détails que je devrais utiliser lors de la création du client.

  • Quelles devraient être les étendues autorisées? (Devraient-ils correspondre au réel champs d'application)

Également lors de la construction de IdentityServerWebHostBuilder

  • Que dois-je transmettre à .AddApiResources ? (Peut-être une question stupide mais est-ce important)

Si quelqu'un peut me guider, ce serait grandement apprécié.

Voici mon test:

public class Startup
{

    public IConfiguration Configuration { get; }
    public static HttpMessageHandler BackChannelHandler { get; set; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        ConfigureAuth(services);    
        services.AddTransient<IPassportService, PassportService>();
        services.Configure<ApiBehaviorOptions>(options =>
        {
            options.SuppressModelStateInvalidFilter = true;
        });

    }

    protected virtual void ConfigureAuth(IServiceCollection services)
    {
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
                options.Audience = Configuration.GetValue<string>("IdentityServerAudience");
                options.BackchannelHttpHandler = BackChannelHandler;
            });
    }


    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }

        app.UseAuthentication();
        app.UseHttpsRedirection();
        app.UseMvc();
        app.UseExceptionMiddleware();
    }
}

Ma classe Startup:

[Fact]
public async Task Attempt_To_Test_InMemory_IdentityServer()
{
    // Create a client
        var clientConfiguration = new ClientConfiguration("MyClient", "MySecret");

        var client = new Client
        {
            ClientId = clientConfiguration.Id,
            ClientSecrets = new List<Secret>
            {
                new Secret(clientConfiguration.Secret.Sha256())
            },
            AllowedScopes = new[] { "api1" },
            AllowedGrantTypes = new[] { GrantType.ClientCredentials },
            AccessTokenType = AccessTokenType.Jwt,
            AllowOfflineAccess = true
        };

        var webHostBuilder = new IdentityServerWebHostBuilder()
            .AddClients(client)
            .AddApiResources(new ApiResource("api1", "api1name"))
            .CreateWebHostBuilder();

        var identityServerProxy = new IdentityServerProxy(webHostBuilder);
        var tokenResponse = await identityServerProxy.GetClientAccessTokenAsync(clientConfiguration, "api1");

        // *****
        // Note: creating an IdentityServerProxy above in order to get an access token
        // causes the next line to throw an exception stating: WebHostBuilder allows creation only of a single instance of WebHost
        // *****

        // Create an auth server from the IdentityServerWebHostBuilder 
        HttpMessageHandler handler;
        try
        {
            var fakeAuthServer = new TestServer(webHostBuilder);
            handler = fakeAuthServer.CreateHandler();
        }
        catch (Exception e)
        {
            throw;
        }

        // Create an auth server from the IdentityServerWebHostBuilder 
        HttpMessageHandler handler;
        try
        {
            var fakeAuthServer = new TestServer(webHostBuilder);
            handler = fakeAuthServer.CreateHandler();
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }

        // Set the BackChannelHandler of the 'production' IdentityServer to use the 
        // handler form the fakeAuthServer
        Startup.BackChannelHandler = handler;
        // Create the apiServer
        var apiServer = new TestServer(new WebHostBuilder().UseStartup<Startup>());
        var apiClient = apiServer.CreateClient();


        apiClient.SetBearerToken(tokenResponse.AccessToken);

        var user = new User
        {
            Username = "simonlomax@ekm.com",
            Password = "Password-123"
        };

        var req = new HttpRequestMessage(new HttpMethod("GET"), "/api/users/login")
        {
            Content = new StringContent(JsonConvert.SerializeObject(user), Encoding.UTF8, "application/json"),
        };

        // Act
        var response = await apiClient.SendAsync(req);

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

}


4 commentaires

Pouvez-vous s'il vous plaît ajouter le code utilisé pour la demande de jeton réelle? Quelle est l'erreur que vous recevez?


@alsami J'obtiens un "non autorisé", ce qui a du sens parce que je ne transmettais pas de jeton porteur, j'ai donc ajouté du code qui, selon moi, ferait cela, mais qui pose maintenant d'autres problèmes que j'espère avoir expliqué dans les commentaires dans le code mis à jour.


@alsami Bien que je puisse maintenant obtenir un jeton d'accès, je ne sais pas comment câbler mon TestServer pour l'API avec IdentityServerProxy


Pouvez-vous fournir le code source complet sur github? J'aurais besoin de tester manuellement pour voir ce qui ne fonctionne pas.


3 Réponses :


3
votes

La suggestion ci-dessous posait un problème. Le code source d'origine a échoué en raison d'une exception en essayant de construire WebHostBuilder deux fois . Deuxièmement, le fichier de configuration n'était présent que dans le projet d'API, pas dans le projet de test, c'est pourquoi l'autorité n'a pas été définie également.

Au lieu de le faire

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
   .AddIdentityServerAuthentication(options =>
   {
      options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
      options.JwtBackChannelHandler = BackChannelHandler;
    });


11 commentaires

Merci pour votre suggestion, mais je n'arrivais pas à faire fonctionner cela non plus. J'ai ajouté un git repo comme vous l'avez demandé. C'est une API super simple qui utilise IdentityServer pour l'authentification. Le projet a également un projet IntegrationTest qui comprend le test que j'ai publié ici. Je vous serais extrêmement reconnaissant si vous pouviez jeter un coup d'œil et me dire où je vais mal.


vous a obtenu une demande de fusion qui l'a corrigé: github.com/simax/SuperSimpleAPI/pull/1 < / a>


C'est génial. Je vous remercie beaucoup pour votre aide. Une chose - parmi d'autres :) qui me déroutait, c'est que je n'avais pas réalisé que je devais appeler CreateHandler () depuis identityServerProxy.IdentityServer Je pense que je le cherchais directement sur identityServerProxy . Encore une fois, merci beaucoup pour la création du paquet nuget et pour votre aide - c'est très apprécié.


@alsami J'ai essayé votre solution, github.com/simax/SuperSimpleAPI et je continue à obtenir http 302. J'ai postulé comme vous l'avez décrit. Une idée pourquoi?


@Ktt vous pouvez fournir un dépôt pour que je puisse le reproduire. Hors de ma tête, je n'ai aucune réponse pourquoi cela se produit.


@alsami Je l'ai surmonté, notre initialisation IOC est beaucoup plus compliquée que le code que vous avez fourni, essayez éventuellement après essayer, j'ai dû supprimer la ligne app.UseIdentityServer () de TestStartup et utiliser IdentityServerProxy et son gestionnaire comme dans votre exemple. Je pense écrire un article de blog à ce sujet. Je partagerai le lien avec vous quand je le ferai. Votre exemple a été d'une grande aide. Merci beaucoup


@Ktt heureux que cela ait fonctionné! Ouais, j'y pensais aussi. Un autre exemple peut être trouvé ici btw: github.com/cleancodelabs/…


@alsami, voici ce que j'ai publié, j'ai également référencé votre code dans github. medium.com/@kutlu_eren/…


Merci! Je vais le partager!


Vous pouvez également ajouter une référence au package :) nuget.org /packages/IdentityServer4.Contrib.AspNetCore.Testin‌ g


@alsami, bien sûr, merci encore :) Veuillez trouver la référence dans le blog où il se trouve comme "Vous voudrez peut-être vérifier ses implémentations impressionnantes ici."



0
votes

Si vous ne voulez pas vous fier à une variable statique pour contenir le HttpHandler, j'ai trouvé que ce qui suit fonctionne. Je pense que c'est beaucoup plus propre.

Commencez par créer un objet que vous pouvez instancier avant que votre TestHost ne soit créé. En effet, vous n'aurez pas le HttpHandler avant la création du TestHost, vous devez donc utiliser un wrapper.

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;

Puis

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


0 commentaires

1
votes

Une solution qui n'affecte pas le code de production:

 _factory = new WebApplicationFactory<Startup>()
        {
            ClientOptions = {BaseAddress = new Uri("http://localhost:5000/")}
        };

        _apiFactory = new TestApiWebApplicationFactory<SampleApi.Startup>(_factory.CreateClient())
        {
            ClientOptions = {BaseAddress = new Uri("http://localhost:5001/")}
        };

et son utilisation est:

public class TestApiWebApplicationFactory<TStartup>
    : WebApplicationFactory<TStartup> where TStartup : class
{
    private readonly HttpClient _identityServerClient;

    public TestApiWebApplicationFactory(HttpClient identityServerClient)
    {
        _identityServerClient = identityServerClient;
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        base.ConfigureWebHost(builder);

        builder.ConfigureServices(
            s =>
            {
                s.AddSingleton<IConfigureOptions<JwtBearerOptions>>(services =>
                {
                    return new TestJwtBearerOptions(_identityServerClient);
                });
            });
    }
}

Les TestJwtBearerOptions ne font que faire passer les requêtes à identityServerClient. La mise en œuvre que vous pouvez trouver ici: https://gist.github.com/ru-sh/048e155d73263912297f1de1539a2687 p >


0 commentaires