ASP.NET Core MVC Testing And The Synchronizer Token Pattern


Have you ever try to call directly your ASP.NET MVC Post actions from your code (from a test method for instance)? If yes, according to your action configuration, you may had to deal with a Bad Request http status...
I had exactly the same problem when I wrote this post about ASP.NET Core MVC testing.

The Problem

This is a very standard behavior. Indeed, one of the most common security best practices in ASP.NET MVC is to protect your controllers from Cross Site Request Forgery (CSRF or Sea Surf) attacks.

This type of attack is intended to send request to a vulnerable site from a malicious one using the current logged user security information to access protected resources.

To preempt this kind of problem, one solution is to generate a token from the server that is sent back to the client (in ASP.NET, a hidden field is generated in the view which is returned to the client). Thus, when the client sends a request, the token has to be included within the content to be validated by the server, otherwise the request is aborted. To validate the token, the server must associate it with the user before returning it using a "user-session" mechanism or a cookie. The server then just compare the associated token with the one sent by the client. This pattern is called the Synchronizer Token Pattern.

The purpose of this post is not to make a deep dive into CSRF protection (see this link for more information about the ASP.NET implementation) but rather explain you how to deal with it within a test context with ASP.NET Core. Indeed, if you want to test your POST methods from an integration test or a behavior test with ASP.NET (see this post for further details about behavior testing), you have to figure out the Anti Forgery Token problem.

The Solution

To make the test successful, we have to simulate the Synchronizer Token Pattern with regard to ASP.NET implementation.

Workflow

Here is a solution workflow:
1. Get the form view from the server and extract the token from the content
2. Serialize the data we want to send to a url encoded string
3. Send the POST request by injecting the token into the serialized data and the cookie collection.

Implementation

I have compiled the implementation as a single extension of the PostAsJsonAsync<> method:

public static class HttpClientHelper
{
    public static async Task<HttpResponseMessage> PostAsJsonAntiForgeryAsync<TContent>(this HttpClient httpClient, string requestUri, TContent content)
    {
        // Get the form view
        HttpResponseMessage responseMsg = await httpClient.GetAsync(requestUri);
        if (!responseMsg.IsSuccessStatusCode)
        {
            return responseMsg;
        }

        // Extract Anti Forgery Token
        var antiForgeryToken = await responseMsg.ExtractAntiForgeryTokenAsync();

        // Serialize data to Key/Value pairs
        IDictionary<string, string> contentData = content.ToKeyValue();

        // Create the request message with previously serialized data + the Anti Forgery Token
        contentData.Add("__RequestVerificationToken", antiForgeryToken);
        var requestMsg = new HttpRequestMessage(HttpMethod.Post, requestUri)
        {
            Content = new FormUrlEncodedContent(contentData)
        };

        // Copy the cookies from the response (containing the Anti Forgery Token) to the request that is about to be sent
        requestMsg.CopyCookiesFromResponse(responseMsg);

        return await httpClient.SendAsync(requestMsg);
    }
}

To make this helper alive, I have reused the excellent work of Stephan hendriks about integration testing in ASP.NET Core especially for the ExtractAntiForgeryTokenAsync and the CopyCookiesFromResponse method.
For the data serialization implementation (method ToKeyValue), take a look at this previous post.

Let's see now how to use it from a test:

[TestMethod]
public void CreateDataShouldBeOk()
{
    var webHostBuilder = new WebHostBuilder();    
    webHostBuilder.UseStartup<[STARTUP_CLASS]>();
    var testServer = new TestServer(webHostBuilder);

    testServer.CreateClient()
              .PostAsJsonAntiForgeryAsync(
                "/api/data/create",
                new Data { Id = 1, Content = [SOME_DATA] })
              .Wait();

    using (var serviceScope = base.TestEnvironment.ServiceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
    {
        Assert.IsNotNull(serviceScope.ServiceProvider
                                     .GetService<[DBCONTEXT_TYPE]>()
                                     .Datas
                                     .FirstOrDefault(d => d.Id == 1));
    }
}

Almost transparent for our test, this is the way I like testing !

If you want more information about how to test you ASP.NET Core application, take a look at theses posts:

Arnaud

Arnaud

I am a Developer/Tech-Lead freelance. Mainly focused on Mobile, Web and Cloud development, I'm simply a technology enthusiast who love learn and share new stuff.

Read More