loading...

ASP.NET Core Integration Testing: Protected endpoints

kaos profile image Kai Oswald ・4 min read

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
[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();
    }
}

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

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

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

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

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

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")]

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

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

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

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.

Posted on by:

Discussion

pic
Editor guide
 

Hi,

I'd like to mock the cookie that the

awat Request.HttpContext.SignInAsync("Cookies", claimsPrincipal);

gives back, in order to test the endpoints protected with the Authorize attribute. I don't want to call the login endpoint just before every test scenario to have a valid cookie, just want to mock it.
How can I achieve it?
Thank in advance.