14

I a have a very simple app with one JWT authenticated controller:

[ApiController]
[Authorize]
[Route("[controller]")]
public class JwtController : ControllerBase
{

    public JwtController() { }

    [HttpGet]
    public ActionResult Get() => Ok("Working!");
}

With the authentication configured as:

services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
    x.RequireHttpsMetadata = false;
    x.SaveToken = true;
    x.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,
        ValidateAudience = false
    };
});

During tests, i want the user to be "authenticated" all the time so that [Authorize] would be skipped.

[Fact]
public async Task JwtIsSkipped()
{
    var response = (await _Client.GetAsync("/jwt")).EnsureSuccessStatusCode();
    var stringResponse = await response.Content.ReadAsStringAsync();

    Assert.Equal("Working!", stringResponse);
}

Running the test like this will fail, so following this doc I added this simple auth handler:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public const string DefaultScheme = "Test";
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, DefaultScheme);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, DefaultScheme);

        return Task.FromResult(AuthenticateResult.Success(ticket));
    }
}

So now my test class looks like this:

public class UnitTest : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly WebApplicationFactory<Startup> _Factory;
    private readonly HttpClient _Client;

    public UnitTest(WebApplicationFactory<Startup> factory)
    {
        _Factory = factory;
        _Client = _Factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(TestAuthHandler.DefaultScheme)
                        .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                            TestAuthHandler.DefaultScheme, options => { });
            });
        }).CreateClient();

        _Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.DefaultScheme);
    }

    [Fact]
    public async Task JwtIsSkipped()
    {
        var response = (await _Client.GetAsync("/jwt")).EnsureSuccessStatusCode();
        var stringResponse = await response.Content.ReadAsStringAsync();

        Assert.Equal("Working!", stringResponse);
    }
}

And it still fails, I have no idea what I'm doing wrong.

2 Answers2

11

I have had similar situation previously with Microsoft example and can promise you it can gives headaches, it may works on specific Core versions, but I have gave up. I have solved this way.

What my goal was, is to Authorize system while testing, instead of using AddAuthentication in our test we just create a FakePolicyEvaluator class and added it as a singleton to our test.

So let go to our FakePolicyEvaluator class:

public class FakePolicyEvaluator : IPolicyEvaluator
{
    public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
    {
        var principal = new ClaimsPrincipal();
        principal.AddIdentity(new ClaimsIdentity(new[] {
            new Claim("Permission", "CanViewPage"),
            new Claim("Manager", "yes"),
            new Claim(ClaimTypes.Role, "Administrator"),
            new Claim(ClaimTypes.NameIdentifier, "John")
        }, "FakeScheme"));
        return await Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal,
            new AuthenticationProperties(), "FakeScheme")));
    }

    public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy,
        AuthenticateResult authenticationResult, HttpContext context, object resource)
    {
        return await Task.FromResult(PolicyAuthorizationResult.Success());
    }
}

Then in our ConfigureTestServices we added services.AddSingleton<IPolicyEvaluator, FakePolicyEvaluator>();

So in your test code like this:

private readonly HttpClient _client;

public UnitTest(WebApplicationFactory<Startup> factory)
{
    _client = factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddSingleton<IPolicyEvaluator, FakePolicyEvaluator>();
        });
    }).CreateClient();
}

[Fact]
public async Task JwtIsSkipped()
{
    var response = (await _client.GetAsync("/jwt")).EnsureSuccessStatusCode();
    var stringResponse = await response.Content.ReadAsStringAsync();

    Assert.Equal("Working!", stringResponse);
}

That is it. Now when you test, it will bypass authentication. I have testing it with the provided controller and it works.

It is also possible to place the fake inside application startup, and it will be both testable for test and working under development environment. Check the referenced article.

Disclaimer: I have wrote in more depth article about this in my personal website Reference you can download a github version of this https://github.com/maythamfahmi/BlogExamples/tree/master/BypassAuthorization

maytham-ɯɐɥʇʎɐɯ
  • 21,551
  • 10
  • 85
  • 103
5

You need to set DefaultAuthenticateScheme

builder.ConfigureTestServices(services =>
{
    services.AddAuthentication(options =>
    {
        x.DefaultAuthenticateScheme = TestAuthHandler.DefaultScheme;
        x.DefaultScheme = TestAuthHandler.DefaultScheme;
    }).AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
           TestAuthHandler.DefaultScheme, options => { });
});
Kahbazi
  • 11,634
  • 1
  • 38
  • 62
  • yes yes this is the trick, just add another handler and set is as default in your test setup – vip32 Oct 02 '20 at 15:40