ASP.NET Core Integration Testing: Protected endpoints

kaos

Kai Oswald

Posted on October 7, 2019

ASP.NET Core Integration Testing: Protected endpoints

In this post I'll describe how you can test protected API endpoints.
I'll use the two most common scenarios: Cookie & JWT Authentication.

I have also created a public repo with the full code. If you want to follow step by step you can also look at the commit history.

Set up the IntegrationTestInitializer

If you've read my introduction to integration testing in ASP.NET Core you'll notice that we've changed the IntegrationTestInitializer quite a bit, because a lot of work has been put into Microsoft.AspNetCore.Mvc.Testing to allow for more configuration. By default the TestServer didn't handle cookies automatically, but when inheriting from WebApplicationFactory<T> the cookies are handled by default. This also cleans up the code.


> Install-Package Microsoft.AspNetCore.Mvc.Testing
Enter fullscreen mode Exit fullscreen mode
[TestClass]
public abstract class IntegrationTestInitializer : WebApplicationFactory<Startup>
{
    protected HttpClient _client;

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Testing")
            .UseStartup<Startup>();
        base.ConfigureWebHost(builder);
    }

    [TestInitialize]
    public void Setup()
    {
        var builder = new WebHostBuilder()
            .UseEnvironment("Testing")
            .UseStartup<Startup>();

        _client = this.CreateClient();
    }
}
Enter fullscreen mode Exit fullscreen mode

Note that we've also set a custom environment Testing for our WebHost.
This will be used so we can decouple our testing code from our production code.

Cookie Authentication

The cookie authentication setup in the API could look similiar to this:

services.AddAuthentication(o => o.DefaultScheme = "Cookies")
    .AddCookie("Cookies", o =>
    {
        o.Cookie.Name = "auth_cookie";
        o.Cookie.HttpOnly = true;
        o.Events = new CookieAuthenticationEvents
        {
            OnRedirectToLogin = redirectContext =>
            {
                redirectContext.HttpContext.Response.StatusCode = 401;
                return Task.CompletedTask;
            }
        };
    });
Enter fullscreen mode Exit fullscreen mode

This configuration basically sets up our HTTP Only Cookie scheme that returns a 401 on unauthenticated requests (by default a 302 redirect to the login page would be returned).

Then the login Method:

[HttpPost("Login")]
public async Task<IActionResult> Login([FromBody] UserLoginModel user)
{
    ClaimsPrincipal claimsPrincipal = null;

    // Return a test user when environment is our Test environment
    if (_env.EnvironmentName == "Testing")
    {
        var claimsIdentity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, user.UserName)
        }, "Cookies");

        claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
    }
    else
    {
        // you should validate the userName and password here and load the specified user
        // We'll just return a default user to keep things simple...
    }
    await Request.HttpContext.SignInAsync("Cookies", claimsPrincipal);
    return NoContent();
}
Enter fullscreen mode Exit fullscreen mode

Note that we just return a default user when we're in the test environment for now.
In a future post I'll also cover invalid users and a simple Database setup.

Testing the unauthorized client

With the authentication method in tact, you should also test if the protected endpoints behave correctly when the client is unauthorized. We expect a HTTP status code of 401 if an unauthorized client tries to access an authorized endpoint.

[TestMethod]
public async Task GetUsersUnauthorizedShouldReturn401()
{
    var response = await _client.GetAsync("api/users");

    Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
}
Enter fullscreen mode Exit fullscreen mode

Testing the authorized client

In C# the HttpClient handles cookies by default, so all we need to do now is to make a POST request to the Login method providing user credentials before we can access protected endpoints.

private async Task PerformLogin(string userName, string password)
{
    var user = new UserLoginModel
    {
        UserName = userName,
        Password = password
    };

    var res = await _client.PostAsJsonAsync("api/account/login", user);
}
Enter fullscreen mode Exit fullscreen mode
[TestMethod]
public async Task CanGetUsers()
{
    List<string> expectedResponse = new List<string> { "Foo", "Bar", "Baz" };

    await PerformLogin("Test", "hunter2");

    var responseJson = await _client.GetStringAsync("api/users");
    List<string> actualResponse = JsonConvert.DeserializeObject<List<string>>(responseJson);

    CollectionAssert.AreEqual(expectedResponse, actualResponse);
}
Enter fullscreen mode Exit fullscreen mode

JWT Authentication

To set up JWT Authentication we have to add it as an Authentication Scheme in our Startup.cs

// Configure Authentication
services.AddAuthentication(o => o.DefaultScheme = "Cookies")
    .AddCookie("Cookies", o =>
    {
        // omitted
    })
    .AddJwtBearer("Token", o => 
    {
        var key = Encoding.ASCII.GetBytes(appSettings.Secret);
        o.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false
        };
    });
Enter fullscreen mode Exit fullscreen mode

I won't go into too much detail how to set up JWT Authentication, but you can look at the GitHub repository containing the full source code. Note that we now have 2 valid Authentication methods active: Cookie & JWT, with Cookies being the default. So if we want to support both authentication methods we'd have to mark our Controller/Action with both authentication schemes.

 [Authorize(AuthenticationSchemes= "Cookies,Token")]
Enter fullscreen mode Exit fullscreen mode

Testing the unauthorized client

Let's write our test with a random token where the authentication should fail.

[TestMethod]
public async Task GetUsersJwtInvalidTokenShouldReturnUnauthorized()
{
    _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "invalid_token");

    var response = await _client.GetAsync("api/users");
    Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
}
Enter fullscreen mode Exit fullscreen mode

Testing the authorized client

To obtain a valid bearer token, we have to make a POST request to our token endpoint providing our credentials.

private async Task<string> GetToken(string userName, string password)
{
    var user = new UserLoginModel
    {
        UserName = userName,
        Password = password
    };

    var res = await _client.PostAsJsonAsync("api/account/token", user);

    if(!res.IsSuccessStatusCode) return null;

    var userModel = await res.Content.ReadAsAsync<User>();

    return userModel?.Token;
}
Enter fullscreen mode Exit fullscreen mode

Then we can set the token for the Authorization header on the test client.

[TestMethod]
public async Task GetUsersJwtInvalidTokenShouldReturnUnauthorized()
{
    _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "invalid_token");

    var response = await _client.GetAsync("api/users");
    Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Now we can test Cookie and JWT protected API endpoints.

In the next post I'll describe how you can test database access using an in-memory database approach.

💖 💪 🙅 🚩
kaos
Kai Oswald

Posted on October 7, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related