Search code examples
c#asp.net-coreintegration-testing.net-6.0

ASP.NET Integration test override authentication still calling other auth handler


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?


Solution

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