Home > Software design >  ASP.NET Integration test override authentication still calling other auth handler
ASP.NET Integration test override authentication still calling other auth handler

Time:07-01

I have the following setup where I have two potential authentication schemes in my web api. They are used for different controllers and use a different auth method. I define them here:

//startup.cs
services.AddAuthentication(options =>
{
    options.DefaultScheme = MarsAuthConstants.Scheme;
})
.AddScheme<MarsAuthSchemeOptions, MarsAuthHandler>(MarsAuthConstants.Scheme, options =>
{
})
.AddScheme<MarsProjectAuthSchemeOptions, MarsProjectAuthHandler>(MarsAuthConstants.ProjectScheme, options =>
{
});

The scheme's names are "Mars" and "MarsProject" respectively.

I'm using Microsoft's instructions to set up mock authentication scheme in my integration tests like so:

    [Fact]
    public async Task GetProject_Http_Test()
    {
        var client = Factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = "Test";
                    options.DefaultChallengeScheme = "Test";
                }).AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    "Test", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test");

        var res = await client.GetAsync("/api/projects/"   ProjectId);
        res.StatusCode.Should().NotBe(HttpStatusCode.Unauthorized);
        res.StatusCode.Should().Be(HttpStatusCode.OK);
    }

However, this test is failing because it's returning 401 Unauthorized. When I debug through, I see that even though I'm configuring the "Test" authorization scheme, it's still calling my MarsAuthHandler.

Since I have 2 controllers that need to be authenticated differently, I have the to specify [Authorize(scheme)] on them, e.g.:


    [HttpGet]
    [Route("{id}")]
    [Authorize(AuthenticationSchemes = MarsAuthConstants.ProjectScheme)]
    public async Task<ProjectView> GetProjectAsync(long id)
    {
        return await _presentationService.GetProjectAsync(id);
    }

If I remove the [Authorize] then my test passes. However, I need this because I cannot specify a single default auth scheme for my entire application.

How can I bypass the [Authorize] attribute when using the Test auth scheme so that my tests can bypass the normal auth methodology?

CodePudding user response:

Since your authorize attribute explicitly using the MarsAuthConstants.ProjectScheme, there is no built-in "easy" way to mock it.

  • There is no "magic" inside ConfigureTestServices, it simply adds additional service configuration for mock dependencies.

So, you have several options:

  1. Override (hacky) the original schema handler with your test handler
    [Fact]
    public async Task GetProject_Http_Test()
    {
        var client = Factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(options =>
                {
                     var authSchemeBuilderMars = new AuthenticationSchemeBuilder("Mars");
                     authSchemeBuilderMars.HandlerType = typeof(TestAuthHandler);

                     var authSchemeBuilderMarsProject = new AuthenticationSchemeBuilder("MarsProject");
                     authSchemeBuilderMarsProject.HandlerType = typeof(TestAuthHandler);

                     // Override already registered schemas
                     o.Schemes.Where(s => s.Name == "Mars").First().HandlerType = typeof(TestAuthHandler);
                     o.Schemes.Where(s => s.Name == "MarsProject").First().HandlerType = typeof(TestAuthHandler);
                     o.SchemeMap["Mars"] = authSchemeBuilderMars;
                     o.SchemeMap["MarsProject"] = authSchemeBuilderMarsProject;
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test");

        var res = await client.GetAsync("/api/projects/"   ProjectId);
        res.StatusCode.Should().NotBe(HttpStatusCode.Unauthorized);
        res.StatusCode.Should().Be(HttpStatusCode.OK);
    }
  1. Use authorization policies. Fortunately, they can be overridden:

In your code setup (example for Mars scheme):

builder.Services.AddAuthorization(options => 
  options.AddPolicy("MarsPolicy", policyBuilder =>
    {
       policyBuilder.RequireAuthenticatedUser();
       policyBuilder.AuthenticationSchemes.Add("Mars");
    }));

In your controller:

[Authorize(Policy = "MarsPolicy")]

In your test code:

    [Fact]
    public async Task GetProject_Http_Test()
    {
        var client = Factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = "Test";
                    options.DefaultChallengeScheme = "Test";
                }).AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    "Test", options => { });
            });
                // This will override the 'MarsPolicy' policy
                services.AddAuthorization(options => 
                   options.AddPolicy("MarsPolicy", policyBuilder =>
                   {
                   policyBuilder.RequireAuthenticatedUser();
                   policyBuilder.AuthenticationSchemes.Add("Test");
                   }));
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test");

        var res = await client.GetAsync("/api/projects/"   ProjectId);
        res.StatusCode.Should().NotBe(HttpStatusCode.Unauthorized);
        res.StatusCode.Should().Be(HttpStatusCode.OK);
    }
  • Related