Problems implementing ValidatingAntiForgeryToken attribute for Web API with MVC 4 RC
Asked Answered
L

5

21

I'm making JSON-based AJAX requests and, with MVC controllers have been very grateful to Phil Haack for his Preventing CSRF with AJAX and, Johan Driessen's Updated Anti-XSRF for MVC 4 RC. But, as I transition API-centric controllers to Web API, I'm hitting issues where the functionality between the two approaches is markedly different and I'm unable to transition the CSRF code.

ScottS raised a similar question recently which was answered by Darin Dimitrov. Darin's solution involves implementing an authorization filter which calls AntiForgery.Validate. Unfortunately, this code does not work for me (see next paragraph) and - honestly - is too advanced for me.

As I understand it, Phil's solution overcomes the problem with MVC AntiForgery when making JSON requests in the absence of a form element; the form element is assumed/expected by the AntiForgery.Validate method. I believe that this may be why I'm having problems with Darin's solution too. I receive an HttpAntiForgeryException "The required anti-forgery form field '__RequestVerificationToken' is not present". I am certain that the token is being POSTed (albeit in the header per Phil Haack's solution). Here's a snapshot of the client's call:

$token = $('input[name=""__RequestVerificationToken""]').val();
$.ajax({
    url:/api/states",
    type: "POST",
    dataType: "json",
    contentType: "application/json: charset=utf-8",
    headers: { __RequestVerificationToken: $token }
}).done(function (json) {
    ...
});

I tried a hack by mashing together Johan's solution with Darin's and was able to get things working but am introducing HttpContext.Current, unsure whether this is appropriate/secure and why I can't use the provided HttpActionContext.

Here's my inelegant mash-up.. the change is the 2 lines in the try block:

public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
    try
    {
        var cookie = HttpContext.Current.Request.Cookies[AntiForgeryConfig.CookieName];
        AntiForgery.Validate(cookie != null ? cookie.Value : null, HttpContext.Current.Request.Headers["__RequestVerificationToken"]);
    }
    catch
    {
        actionContext.Response = new HttpResponseMessage
        {
            StatusCode = HttpStatusCode.Forbidden,
            RequestMessage = actionContext.ControllerContext.Request
        };
        return FromResult(actionContext.Response);
    }
    return continuation();
}

My questions are:

  • Am I correct in thinking that Darin's solution assumes the existence of a form element?
  • What's an elegant way to mash-up Darin's Web API filter with Johan's MVC 4 RC code?

Thanks in advance!

Likeminded answered 30/7, 2012 at 16:49 Comment(1)
whats the FromResult?Timecard
K
35

You could try reading from the headers:

var headers = actionContext.Request.Headers;
var cookie = headers
    .GetCookies()
    .Select(c => c[AntiForgeryConfig.CookieName])
    .FirstOrDefault();
var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);

Note: GetCookies is an extension method that exists in the class HttpRequestHeadersExtensions which is part of System.Net.Http.Formatting.dll. It will most likely exist in C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies\System.Net.Http.Formatting.dll

Kenishakenison answered 30/7, 2012 at 17:32 Comment(4)
Hey... What about GetCookies() method? How we have to implement it in general?Jorgan
From MVC Source code, I have seen that they are using httpContext.Request.Form[_config.FormFieldName] in the GetFormToken method. Is there any reason why to not use Request.Params to access both form & header values? I think it may also include cookie :(. However they could check the request header when the Form is not available...Clap
The HttpRequestHeadersExtensions DLL, System.Net.Http.Formatting.dll, also comes from the Nuget package Microsoft.AspNet.WebApi.Client.Tientiena
Likewise, AntiForgery and AntiForgeryConfig come from System.Web.WebPages.dll which is in the Microsoft.AspNet.WebPages nuget package.Tientiena
V
13

Just wanted to add that this approach worked for me also (.ajax posting JSON to a Web API endpoint), although I simplified it a bit by inheriting from ActionFilterAttribute and overriding the OnActionExecuting method.

public class ValidateJsonAntiForgeryTokenAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        try
        {
            var cookieName = AntiForgeryConfig.CookieName;
            var headers = actionContext.Request.Headers;
            var cookie = headers
                .GetCookies()
                .Select(c => c[AntiForgeryConfig.CookieName])
                .FirstOrDefault();
            var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
            AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
        }
        catch
        {               
            actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Unauthorized request.");
        }
    }
}
Vanquish answered 18/4, 2013 at 20:7 Comment(0)
A
0

Extension method using Darin's answer, with a check for the presence of the header. The check means that the resulting error message is more indicative of what's wrong ("The required anti-forgery form field "__RequestVerificationToken" is not present.") versus "The given header was not found."

public static bool IsHeaderAntiForgeryTokenValid(this HttpRequestMessage request)
{
    try
    {
        HttpRequestHeaders headers = request.Headers;
        CookieState cookie = headers
                .GetCookies()
                .Select(c => c[AntiForgeryConfig.CookieName])
                .FirstOrDefault();

        var rvt = string.Empty;
        if (headers.Any(x => x.Key == AntiForgeryConfig.CookieName))
            rvt = headers.GetValues(AntiForgeryConfig.CookieName).FirstOrDefault();

        AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
    }
    catch (Exception ex)
    {
        LogHelper.LogError(ex);
        return false;
    }

    return true;
}

ApiController Usage:

public IHttpActionResult Get()
{
    if (Request.IsHeaderAntiForgeryTokenValid())
        return Ok();
    else
        return BadRequest();
}
Atrip answered 15/12, 2014 at 22:22 Comment(0)
C
0

An implementation using AuthorizeAttribute:

using System;
using System.Linq;
using System.Net.Http;
using System.Web;
using System.Web.Helpers;
using System.Web.Http;
using System.Web.Http.Controllers;

  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
  public class ApiValidateAntiForgeryToken : AuthorizeAttribute {
    public const string HeaderName = "X-RequestVerificationToken";

    private static string CookieName => AntiForgeryConfig.CookieName;

    public static string GenerateAntiForgeryTokenForHeader(HttpContext httpContext) {
      if (httpContext == null) {
        throw new ArgumentNullException(nameof(httpContext));
      }

      // check that if the cookie is set to require ssl then we must be using it
      if (AntiForgeryConfig.RequireSsl && !httpContext.Request.IsSecureConnection) {
        throw new InvalidOperationException("Cannot generate an Anti Forgery Token for a non secure context");
      }

      // try to find the old cookie token
      string oldCookieToken = null;
      try {
        var token = httpContext.Request.Cookies[CookieName];
        if (!string.IsNullOrEmpty(token?.Value)) {
          oldCookieToken = token.Value;
        }
      }
      catch {
        // do nothing
      }

      string cookieToken, formToken;
      AntiForgery.GetTokens(oldCookieToken, out cookieToken, out formToken);

      // set the cookie on the response if we got a new one
      if (cookieToken != null) {
        var cookie = new HttpCookie(CookieName, cookieToken) {
          HttpOnly = true,
        };
        // note: don't set it directly since the default value is automatically populated from the <httpCookies> config element
        if (AntiForgeryConfig.RequireSsl) {
          cookie.Secure = AntiForgeryConfig.RequireSsl;
        }
        httpContext.Response.Cookies.Set(cookie);
      }

      return formToken;
    }


    protected override bool IsAuthorized(HttpActionContext actionContext) {
      if (HttpContext.Current == null) {
        // we need a context to be able to use AntiForgery
        return false;
      }

      var headers = actionContext.Request.Headers;
      var cookies = headers.GetCookies();

      // check that if the cookie is set to require ssl then we must honor it
      if (AntiForgeryConfig.RequireSsl && !HttpContext.Current.Request.IsSecureConnection) {
        return false;
      }

      try {
        string cookieToken = cookies.Select(c => c[CookieName]).FirstOrDefault()?.Value?.Trim(); // this throws if the cookie does not exist
        string formToken = headers.GetValues(HeaderName).FirstOrDefault()?.Trim();

        if (string.IsNullOrEmpty(cookieToken) || string.IsNullOrEmpty(formToken)) {
          return false;
        }

        AntiForgery.Validate(cookieToken, formToken);
        return base.IsAuthorized(actionContext);
      }
      catch {
        return false;
      }
    }
  }

Then just decorate your controller or methods with [ApiValidateAntiForgeryToken]

And add to the razor file this to generate your token for javascript:

<script>
var antiForgeryToken = '@ApiValidateAntiForgeryToken.GenerateAntiForgeryTokenForHeader(HttpContext.Current)';
// your code here that uses such token, basically setting it as a 'X-RequestVerificationToken' header for any AJAX calls
</script>
Cyprinid answered 5/1, 2017 at 13:53 Comment(0)
W
0

If it helps anyone, in .net core, the header's default value is actually just "RequestVerificationToken", without the "__". So if you change the header's key to that instead, it'll work.

You can also override the header name if you like:

services.AddAntiforgery(o => o.HeaderName = "__RequestVerificationToken")

Wilmot answered 21/12, 2018 at 0:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.