ValidateAntiForgeryToken with SPA architecture

2019-01-31 14:53发布

I am trying to set Register and Login for Hot Towel SPA applicantion. I have created SimpleMembershipFilters and ValidateHttpAntiForgeryTokenAttribute based on the asp.net single page application template.

How do you get the

 @Html.AntiForgeryToken()

code to work in the Durandal SPA pattern.

Currently I have a register.html

<section>
    <h2 data-bind="text: title"></h2>

    <label>Firstname:</label><input data-bind="value: firstName" type="text"  />
    <label>Lastname:</label><input data-bind="value: lastName" type="text"  />
    <label>Email:</label><input data-bind="value: emailAddress" type="text"  />
    <label>Company:</label><input data-bind="value: company" type="text"  />
    <br />
    <label>Password:</label><input data-bind="value: password1" type="password" />
    <label>Re-Enter Password:</label><input data-bind="value: password2" type="password" />
    <input type="button" value="Register" data-bind="click: registerUser" class="btn" />
</section>

register.js:

define(['services/logger'], function (logger) {
    var vm = {
        activate: activate,
        title: 'Register',
        firstName: ko.observable(),
        lastName: ko.observable(),
        emailAddress: ko.observable(),
        company: ko.observable(),
        password1: ko.observable(),
        password2: ko.observable(),
        registerUser: function () {
            var d = {
                'FirstName': vm.firstName,
                'LastName': vm.lastName,
                'EmailAddress': vm.emailAddress,
                'Company': vm.company,
                'Password': vm.password1,
                'ConfirmPassword': vm.password2
            };
            $.ajax({
                url: 'Account/JsonRegister',
                type: "POST",
                data: d ,
                success: function (result) {
                },
                error: function (result) {
                }
            });
        },
    };


    return vm;

    //#region Internal Methods
    function activate() {
        logger.log('Login Screen Activated', null, 'login', true);
        return true;
    }
    //#endregion
});

In the $ajax call how do I pass the AntiForgeryToken? Also how do I create the token as well?

4条回答
我只想做你的唯一
2楼-- · 2019-01-31 15:17

I struggled a bit with this as neither of the existing answers seemed to work correctly for the case of my Durandal SPA app based on the Hot Towel Template.

I had to use a combination of Evan Larson's and curtisk's answers to get something that worked the way I think its supposed to.

To my index.cshtml page (Durandal supports cshtml alongside html) I added the following just above the </body> tag

@AntiForgery.GetHtml();

I added a custom filter class as suggested by Evan Larson, however I had to modify it to support looking up the cookie value separately and utilize __RequestVerificationToken as the name rather than RequestVerificationToken as this is what is supplied by AntiForgery.GetHtml();

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Helpers;
using System.Web.Http.Filters;
using System.Net.Http;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Threading;
using System.Net.Http.Headers;

namespace mySPA.Filters
{
    public class ValidateJsonAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
    {
        public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            if (actionContext == null)
            {
                throw new ArgumentNullException("HttpActionContext");
            }

            if (actionContext.Request.Method != HttpMethod.Get)
            {
                return ValidateAntiForgeryToken(actionContext, cancellationToken, continuation);
            }

            return continuation();
        }

        private Task<HttpResponseMessage> FromResult(HttpResponseMessage result)
        {
            var source = new TaskCompletionSource<HttpResponseMessage>();
            source.SetResult(result);
            return source.Task;
        }

        private Task<HttpResponseMessage> ValidateAntiForgeryToken(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            try
            {
                string cookieToken = "";
                string formToken = "";
                IEnumerable<string> tokenHeaders;
                if (actionContext.Request.Headers.TryGetValues("__RequestVerificationToken", out tokenHeaders))
                {
                    formToken = tokenHeaders.First();
                }
                IEnumerable<CookieHeaderValue> cookies = actionContext.Request.Headers.GetCookies("__RequestVerificationToken");
                CookieHeaderValue tokenCookie = cookies.First();
                if (tokenCookie != null)
                {
                    cookieToken = tokenCookie.Cookies.First().Value;
                }
                AntiForgery.Validate(cookieToken, formToken);
            }
            catch (System.Web.Mvc.HttpAntiForgeryException ex)
            {
                actionContext.Response = new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.Forbidden,
                    RequestMessage = actionContext.ControllerContext.Request
                };
                return FromResult(actionContext.Response);
            }
            return continuation();
        }
    }
}

Subsequently in my App_Start/FilterConfig.cs I added the following

public static void RegisterHttpFilters(HttpFilterCollection filters)
{
    filters.Add(new ValidateJsonAntiForgeryTokenAttribute());
}

In Application_Start under my Global.asax I added

FilterConfig.RegisterHttpFilters(GlobalConfiguration.Configuration.Filters);

Finally for my ajax calls I added a derivation of curtisk's input lookup to add a header to my ajax request, in the case a login request.

var formForgeryToken = $('input[name="__RequestVerificationToken"]').val();

return Q.when($.ajax({
    url: '/breeze/account/login',
    type: 'POST',
    contentType: 'application/json',
    dataType: 'json',
    data: JSON.stringify(data),
    headers: {
        "__RequestVerificationToken": formForgeryToken
    }
})).fail(handleError);

This causes all of my post requests to require a verification token which is based upon the cookie and hidden form verification tokens created by AntiForgery.GetHtml();

From my understanding this will prevent the potential for cross site scripting attacks as the attacking site would need to be able to supply both the cookie and the hidden form value to be able to verify themselves, which would be far more difficult to acquire.

查看更多
对你真心纯属浪费
3楼-- · 2019-01-31 15:25

I would read this article on how to use antiforgery tokens using javascript. The article is written for WebApi but it can easily applied to an MVC controller if you want to.

The short answer is something like this: Inside your cshtml view:

<script>
    @functions{
        public string TokenHeaderValue()
        {
            string cookieToken, formToken;
            AntiForgery.GetTokens(null, out cookieToken, out formToken);
            return cookieToken + ":" + formToken;                
        }
    }

    $.ajax("api/values", {
        type: "post",
        contentType: "application/json",
        data: {  }, // JSON data goes here
        dataType: "json",
        headers: {
            'RequestVerificationToken': '@TokenHeaderValue()'
        }
    });
</script>

Then inside your asp.net controller you need to validate the token like so:

void ValidateRequestHeader(HttpRequestMessage request)
{
    string cookieToken = "";
    string formToken = "";

    IEnumerable<string> tokenHeaders;
    if (request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders))
    {
        string[] tokens = tokenHeaders.First().Split(':');
        if (tokens.Length == 2)
        {
            cookieToken = tokens[0].Trim();
            formToken = tokens[1].Trim();
        }
    }
    AntiForgery.Validate(cookieToken, formToken);
}

The reason you want to pass it in the headers is because if you pass it as a parameter data parameter in your ajax call, inside the querystring or body, of the request. Then it will be harder for you to get the antiforgery token for all your different scenarios. Because you will have to deserialize the body and then find the token. In the headers its pretty consistent and easy to retrieve.


****edit for ray**

Here is an example of an action filter which you can use to attribute web api methods to validate if a antiforgerytoken is provided.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Helpers;
using System.Web.Http.Filters;
using System.Net.Http;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Threading;

namespace PAWS.Web.Classes.Filters
{
    public class ValidateJsonAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
    {
        public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            if (actionContext == null)
            {
                throw new ArgumentNullException("HttpActionContext");
            }

            if (actionContext.Request.Method != HttpMethod.Get)
            {
                return ValidateAntiForgeryToken(actionContext, cancellationToken, continuation);
            }

            return continuation();
        }

        private Task<HttpResponseMessage> FromResult(HttpResponseMessage result)
        {
            var source = new TaskCompletionSource<HttpResponseMessage>();
            source.SetResult(result);
            return source.Task;
        }

        private Task<HttpResponseMessage> ValidateAntiForgeryToken(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            try
            {
                string cookieToken = "";
                string formToken = "";
                IEnumerable<string> tokenHeaders;
                if (actionContext.Request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders))
                {
                    string[] tokens = tokenHeaders.First().Split(':');
                    if (tokens.Length == 2)
                    {
                        cookieToken = tokens[0].Trim();
                        formToken = tokens[1].Trim();
                    }
                }
                AntiForgery.Validate(cookieToken, formToken);
            }
            catch (System.Web.Mvc.HttpAntiForgeryException ex)
            {
                actionContext.Response = new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.Forbidden,
                    RequestMessage = actionContext.ControllerContext.Request
                };
                return FromResult(actionContext.Response);
            }
            return continuation();
        }
    }
}
查看更多
仙女界的扛把子
4楼-- · 2019-01-31 15:25

If using MVC 5 read this solution!

I tried the above solutions, but they did not work for me, the Action Filter was never reached and I couldn't figure out why. The MVC version is not mentioned above, but I am going to assume it was version 4. I am using version 5 in my project and ended up with the following action filter:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Filters;

namespace SydHeller.Filters
{
    public class ValidateJSONAntiForgeryHeader : FilterAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            string clientToken = filterContext.RequestContext.HttpContext.Request.Headers.Get(KEY_NAME);
            if (clientToken == null)
            {
                throw new HttpAntiForgeryException(string.Format("Header does not contain {0}", KEY_NAME));
            }

            string serverToken = filterContext.HttpContext.Request.Cookies.Get(KEY_NAME).Value;
            if (serverToken == null)
            {
                throw new HttpAntiForgeryException(string.Format("Cookies does not contain {0}", KEY_NAME));
            }

            System.Web.Helpers.AntiForgery.Validate(serverToken, clientToken);
        }

        private const string KEY_NAME = "__RequestVerificationToken";
    }
}

-- Make note of the using System.Web.Mvc and using System.Web.Mvc.Filters, not the http libraries (I think that is one of the things that changed with MVC v5. --

Then just apply the filter [ValidateJSONAntiForgeryHeader] to your action (or controller) and it should get called correctly.

In my layout page right above </body> I have @AntiForgery.GetHtml();

Finally, in my Razor page, I do the ajax call as follows:

var formForgeryToken = $('input[name="__RequestVerificationToken"]').val();

$.ajax({
  type: "POST",
  url: serviceURL,
  contentType: "application/json; charset=utf-8",
  dataType: "json",
  data: requestData,
  headers: {
     "__RequestVerificationToken": formForgeryToken
  },
     success: crimeDataSuccessFunc,
     error: crimeDataErrorFunc
});
查看更多
贪生不怕死
5楼-- · 2019-01-31 15:36

Grab value of token in JS var

var antiForgeryToken = $('input[name="__RequestVerificationToken"]').val();

Then just add to your ajax POST headers in the beforeSend function of the .ajax call

beforeSend: function (xhr, settings) {
            if (settings.data != "") {
                settings.data += '&';
            }
            settings.data += '__RequestVerificationToken=' +  encodeURIComponent(antiForgeryToken);
}
查看更多
登录 后发表回答