ValidateAntiForgeryToken with SPA architecture
Asked Answered
D

4

11

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?

Depreciate answered 14/3, 2013 at 17:33 Comment(0)
T
7

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();
        }
    }
}
Tetragon answered 19/3, 2013 at 23:13 Comment(5)
How do you call ValidateRequestHeader?Indigence
I would create an action filter because this is a cross cutting concern and is a aspect oriented design. This way you can just attribute the methods you want to enforce this security on.Tetragon
FYI: The attribute implements IAuthorizationFilter but the public void OnAuthorization( AuthorizationContext filterContext ) method is missing.Distinction
The constrain with this approach is that: the page has to be a Razor Page .cshtml and my SPA pages/views are simple HTML pages so it won't work for me.Estimative
Jaider, the concept should work for whatever scenario you want it to. Yes the code takes those assumptions but the concepts remain constant. You create a antiforgery token on the server. Pass it to the client upon a request. The client must use this token on any PUT/POST/DELETE http requests by placing this token in an http header. This prevents cross site request forgery.Tetragon
S
3

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);
}
Shote answered 14/3, 2013 at 17:42 Comment(6)
the _RequestVerfitcationToken input i have to add to my code, but how and where can set the value of the input? Is this something that auto generate? Sorry new to this.Depreciate
Yeah,@Html.AntiForgeryToken() will create the hidden input automatically, that is what you are grabbing the value of in jsShote
Since I am using the HotTowel template which uses Durandal. Durandal follows the architecture where it looks for .html file and .js file. The .html file is where we have the all UI elements. The @ call is not avaliable here, since it is staright html file and not a cshtml file.Depreciate
By default it may use straight html, but you can use cshtml under Durandal/Hot TowelShote
ah there we go. I didn't know that. Let me try that.Depreciate
I created a Razor .cshtml file, but none of the Html.Helpers are getting recognized. Error 15 'System.Web.WebPages.Html.HtmlHelper' does not contain a definition for 'AntiForgeryToken' and no extension method 'AntiForgeryToken' accepting a first argument of type 'System.Web.WebPages.Html.HtmlHelper' could be found (are you missing a using directive or an assembly reference?)Depreciate
L
1

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.

Langland answered 27/8, 2013 at 12:26 Comment(2)
I needed to modify to follow to get this to work: Set the cookiename in AntiForgeryConfig.CookieName to __RequestVerificationToken (it was called __RequestVerificationToken_fugh485 on my system)Tessi
Also, tokenCookie.Cookies.First().Value; gave me the wrong cookie. Changed to tokenCookie.Cookies.First(c => c.Name == "__RequestVerificationToken").Value;Tessi
P
1

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
});
Polio answered 27/1, 2017 at 22:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.