Saturday, March 26, 2016

POST-ing to an ASP.NET MVC form with an anti-forgery token

I've had some issues trying to write an acceptance test that was registering a new user by POST-ing the required information to a MVC site and I got back these messages:

  • The required anti-forgery cookie "__RequestVerificationToken" is not present.
  • The required anti-forgery form field "__RequestVerificationToken" is not present.
  • Validation of the provided anti-forgery token failed. The cookie "_RequestVerificationToken" and the form field "_RequestVerificationToken" were swapped.

Since it took me a bit of fiddling with the code before I managed to make it work, I thought I'd share the solution. The site is an ASP.NET MVC version 5 and I am trying to register a new user (POST-ing to the /Account/Register URL). The main issue you will encounter is having to extract two anti-forgery tokens, one from the cookies and one from the form, and then sending both of them in the appropriate places (cookies vs form field).

I have used LinqPad 5 with a reference to the HtmlAgilityPack NuGet package (to simplify extracting the needed information from the GET form). This is the code:

void Main()
{
  Cookie[] cookies;
  var html = Get("Account/Register", out cookies);
  var root = Parse(html);
  var formToken = GetFormToken(root);
  var cookieToken = GetCookieToken(cookies);
  
  var unique = Guid.NewGuid().ToString("N");
  
  Post("Account/Register", new
    {
      Email = unique + "@example.com",
      User = unique,
      Password = unique,
      ConfirmPassword = unique,
    },
      formToken, cookieToken);
}

  private const string BASE_URL = "http://localhost:5972"; // replace as needed  
  private const string TOKEN_NAME = "__RequestVerificationToken";
  
  private static string Get(string url, out Cookie[] responseCookies)
  {
    using (var web = new CookieAwareWebClient())
    {
      var result = web.DownloadString(BASE_URL + "/" + url);
      responseCookies = web.ResponseCookies.Cast<Cookie>().ToArray();

      return result;
    }
  }

  private static string Post(string url, object body, string formToken = null, string cookieToken = null)
  {
    using (var web = new WebClient())
    {
      var data = GetPostData(body);

      web.Headers["Content-Type"] = "application/x-www-form-urlencoded";
      if (formToken != null)
        data += "&" + TOKEN_NAME + "=" + formToken;
      if (cookieToken != null)
        web.Headers.Add(HttpRequestHeader.Cookie, TOKEN_NAME + "=" + cookieToken);

      return web.UploadString(BASE_URL + "/" + url, data);
    }
  }

  private static string GetPostData(object obj)
  {
    var kv = obj
      .GetType()
      .GetProperties(BindingFlags.Instance | BindingFlags.Public)
      .Select(prop => prop.Name + "=" + WebUtility.UrlEncode(prop.GetValue(obj) + ""))
      .ToArray();
    return string.Join("&", kv);
  }

  private static HtmlNode Parse(string html)
  {
    var doc = new HtmlDocument();
    doc.LoadHtml(html);
    return doc.DocumentNode;
  }

  private static string GetFormToken(HtmlNode root)
  {
    var formToken = root.SelectSingleNode("//*[@name='" + TOKEN_NAME + "']");
    return formToken.GetAttributeValue("value", null);
  }

  private static string GetCookieToken(IEnumerable<Cookie> cookies)
  {
    return cookies
      .Where(it => it.Name == TOKEN_NAME)
      .First()
      .Value;
  }

  // from http://stackoverflow.com/a/29479390
  public class CookieAwareWebClient : WebClient
  {
    public CookieContainer CookieContainer { get; }
    public CookieCollection ResponseCookies { get; set; }

    public CookieAwareWebClient()
    {
      CookieContainer = new CookieContainer();
      ResponseCookies = new CookieCollection();
    }

    protected override WebRequest GetWebRequest(Uri address)
    {
      var request = (HttpWebRequest) base.GetWebRequest(address);
      // ReSharper disable once PossibleNullReferenceException
      request.CookieContainer = CookieContainer;
      return request;
    }

    protected override WebResponse GetWebResponse(WebRequest request)
    {
      var response = (HttpWebResponse) base.GetWebResponse(request);
      // ReSharper disable once PossibleNullReferenceException
      ResponseCookies = response.Cookies;
      return response;
    }
  }

Hope this helps someone.

No comments: