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: