Where exactly to put the antiforgeryToken

2019-02-22 13:16发布

问题:

I have a layout page that has a form with AntiForgeryToken

using (Html.BeginForm(action, "Account", new { ReturnUrl = returnUrl }, FormMethod.Post, new { Id = "xcrf-form" }))

This generates a hidden field

<input name="__RequestVerificationToken" type="hidden" value="p43bTJU6xjctQ-ETI7T0e_0lJX4UsbTz_IUjQjWddsu29Nx_UE5rcdOONiDhFcdjan88ngBe5_ZQbHTBieB2vVXgNJGNmfQpOm5ATPbifYE1">

In my angular view (that is loaded in a div in the layout page, I do this

<form class="form" role="form" ng-submit="postReview()">

And my code for postReview() is as follows

$scope.postReview = function () {
    var token = $('[name=__RequestVerificationToken]').val();

    var config = {
        headers: {
            "Content-Type": "multipart/form-data",
            // the following when uncommented does not work either
            //'RequestVerificationToken' : token
            //"X-XSRF-TOKEN" : token
        }
    }

    // tried the following, since my other MVC controllers (non-angular) send the token as part of form data, this did not work though
    $scope.reviewModel.__RequestVerificationToken = token;

    // the following was mentioned in some link I found, this does not work either
    $http.defaults.headers.common['__RequestVerificationToken'] = token;

    $http.post('/Review/Create', $scope.reviewModel, config)
    .then(function (result) {
        // Success
        alert(result.data);
    }, function (error) {
        // Failure
        alert("Failed");
    });
}

My MVC Create method is as follows

    [HttpPost]
    [ValidateAntiForgeryToken]
    [AllowAnonymous]
    public ActionResult Create([Bind(Include = "Id,CommentText,Vote")] ReviewModel reviewModel)
    {
        if (User.Identity.IsAuthenticated == false)
        {
            // I am doing this instead of [Authorize] because I dont want 302, which browser handles and I cant do client re-direction
            return new HttpStatusCodeResult(HttpStatusCode.Forbidden);
        }

        // just for experimenting I have not yet added it to db, and simply returning
        return new JsonResult {Data = reviewModel, JsonRequestBehavior = JsonRequestBehavior.AllowGet};
    }

So no matter where I put the token, no matter what I use for 'Content-Type' (I tried application-json and www-form-urlencoded) I always get the error "The required anti-forgery form field "__RequestVerificationToken" is not present."

I even tried naming __RequestVerificationToken and RequestVerificationToken

Why does my server not find the damn token?

I also looked at couple of links that ask you to implement your own AntiForgeryToeknVerifyAttrbute and verify the token that is sent as cookieToken:formToken, I have not tried that but why I am not able to get it working whereas this works for the MVC controllers (non-angular posts)

回答1:

Yes. By default, MVC Framework will check for Request.Form["__RequestVerificationToken"].

Checking the MVC source code

    public AntiForgeryToken GetFormToken(HttpContextBase httpContext)
    {
        string value = httpContext.Request.Form[_config.FormFieldName];
        if (String.IsNullOrEmpty(value))
        {
            // did not exist
            return null;
        }

        return _serializer.Deserialize(value);
    }

You need to create your own filter to check it from Request.Header

Code Snippet from Phil Haack's Article - MVC 3

private class JsonAntiForgeryHttpContextWrapper : HttpContextWrapper {
  readonly HttpRequestBase _request;
  public JsonAntiForgeryHttpContextWrapper(HttpContext httpContext)
    : base(httpContext) {
    _request = new JsonAntiForgeryHttpRequestWrapper(httpContext.Request);
  }

  public override HttpRequestBase Request {
    get {
      return _request;
    }
  }
}

private class JsonAntiForgeryHttpRequestWrapper : HttpRequestWrapper {
  readonly NameValueCollection _form;

  public JsonAntiForgeryHttpRequestWrapper(HttpRequest request)
    : base(request) {
    _form = new NameValueCollection(request.Form);
    if (request.Headers["__RequestVerificationToken"] != null) {
      _form["__RequestVerificationToken"] 
        = request.Headers["__RequestVerificationToken"];
    }
}

  public override NameValueCollection Form {
    get {
      return _form;
    }
  }
}

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, 
    AllowMultiple = false, Inherited = true)]
public class ValidateJsonAntiForgeryTokenAttribute : 
    FilterAttribute, IAuthorizationFilter {
  public void OnAuthorization(AuthorizationContext filterContext) {
    if (filterContext == null) {
      throw new ArgumentNullException("filterContext");
    }

    var httpContext = new JsonAntiForgeryHttpContextWrapper(HttpContext.Current);
    AntiForgery.Validate(httpContext, Salt ?? string.Empty);
  }

  public string Salt {
    get;
    set;
  }

  // The private context classes go here
}

Check out here for MVC 4 implementation, to avoid salt issue

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class,
                AllowMultiple = false, Inherited = true)]
public sealed class ValidateJsonAntiForgeryTokenAttribute
                            : FilterAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }

        var httpContext = filterContext.HttpContext;
        var cookie = httpContext.Request.Cookies[AntiForgeryConfig.CookieName];
        AntiForgery.Validate(cookie != null ? cookie.Value : null,
                             httpContext.Request.Headers["__RequestVerificationToken"]);
    }
}


回答2:

I had the same problem. Turned out that I don't need to set antiforgery token anywhere explicitly in my angular js code. The MVC controller expects this token value to be delivered from 1. the form field, 2. cookie. The filter equates and is happy when they match. When we submit the form, hidden field for the anti forgery token automatically supplies its value. Cookie is automatically set by the browser. So as I said, we don't need to do anything explicitly.

The problem really is request's content-type. By default it goes as as application/json and therefore the a.f. token value (or rather any form data) is not received. Following worked for me:

// create the controller
var RegisterController = function ($scope, $http) {

    $scope.onSubmit = function (e) {
        // suppress default form submission
        e.preventDefault();
        var form = $("#registerform");

        if (form.valid()) {
            var url = form.attr('action');
            var data = form.serialize();

            var config = {
                headers: {
                    'Content-type':'application/x-www-form-urlencoded',
                }
            };

            $http.post(url, data, config).success(function (data) {
                alert(data);
            }).error(function(reason) {
                alert(reason);
            });

        }
    };
};


回答3:

As Murali suggested I guess I need to put the toekn in the form itself, so I tried putting the token as part of form data and I needed to encode the form data as explained in https://stackoverflow.com/a/14868725/2475810

This approach does not require any additional code on server side, also we do not need to create and join cookie and form token. Just by form-encoding the data and including token as one of the fields as explained in the answer above we can get it rolling.



回答4:

You should perform the HTTP request in this way:

$http({
    url: '/Review/Create',
    data: "__RequestVerificationToken=" + token + "&param1=1&param2=2",
    method: 'POST',
    headers: {
      'X-Requested-With': 'XMLHttpRequest',
      'Accept': 'application/json, text/javascript, */*; q=0.01',
      'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
    }
  }).success(function(result) {
    alert(result.data);
  }).error(function(error) {
    alert("Failed");
  });