Search code examples
c#.netasp.net-mvcasp.net-mvc-3csrf

Get the raw values (not html) from AntiForgeryToken()


This beautiful abstraction lets you place @Html.AntiForgeryToken() in cshtml file which is magically expanded to something like;

<input name="__RequestVerificationToken" type="hidden" value="JjMHm5KJQ/qJsyC4sgifQWWX/WmADmNvEgHZXXuB07bWoL84DrmQzE6k9irVyFSJ5VSYqeUIXgl4Dw4NHSotLwflGYTyECzLvrgzbtonxJ9m3GVPgUV7Z6s2Ih/klUB78GN7Fl4Gj7kxg62MEoGcZw175eVwTmkKJ0XrtEfD5KCVvYIMHNY8MT2l+qhltsGL87c9dII42AVoUUQ2gTvfPg==" />

By mvc before the page is served. However my page has some JavaScript making ajax calls which don't include the token even though it's been added to the form. They are currently getting the expected [HttpAntiForgeryException]: A required anti-forgery token was not supplied or was invalid. because they don't have the token. I'm aware I could parse the value out of the DOM but I shouldn't have to. Are there other ways of accessing/getting this value? To be clear, I mean I'd like an overload of the method that returns just the value as a string or some kind of object that has the name and value both.

To provide a bit more context my form and the relevant JS looks a little like this;

<form action="/settings" method="post"><input name="__RequestVerificationToken" type="hidden" value="JjMHm5KJQ/qJsyC4sgifQWWX/WmADmNvEgHZXXuB07bWoL84DrmQzE6k9irVyFSJ5VSYqeUIXgl4Dw4NHSotLwflGYTyECzLvrgzbtonxJ9m3GVPgUV7Z6s2Ih/klUB78GN7Fl4Gj7kxg62MEoGcZw175eVwTmkKJ0XrtEfD5KCVvYIMHNY8MT2l+qhltsGL87c9dII42AVoUUQ2gTvfPg==" />    <fieldset>
        <h3>User Settings</h3>
        <ul>
            <li>
            label for="password">Password</label>
                <a href="#" id="change_password" class="changePasswordButton">Edit</a>
                <div id="password_section" class="inlineedit">
                    <div>
                        <span for="existing_password">Current password</span> <input autocomplete="off" class="required" id="existing_password" name="existing_password" type="password" />
                    </div>
                    <div>
                        <span for="new_password">New password</span> <input autocomplete="off" class="required" id="new_password" name="new_password" type="password" />
                        <span id="password_strength" />
                    </div>
                    <div>
                        <span for="confirm_password">Confirm password</span> <input autocomplete="off" class="required" id="confirm_password" name="confirm_password" type="password" />
                    </div>
                    <div class="inlinesave">
                        <input type="button" value="Change" onclick="onPostChangePassword();"/>
                        <a href="#" id="cancel_password" class="cancel">Cancel</a>
                    </div>
                </div>
            </li>
    // a bunch more of these that call their own onPostChangeSetting method

onPostChangePassword() does some input validation then;

 if(validPWD && validNewPWD && validConfirmPWD && current_pwd != new_pwd){
                        // Post the password change
                        var currentAjaxRequest = $.ajax({
                            type: "POST",
                            url: "/settings/preferences/changepassword",
                            cache: false,
                            data: {password: $('#new_password').val(), current: $('#existing_password').val(),confirm: $('#confirm_password').val()},
                            success: password_success,
                            error: password_error,
                            dataType: "json"
                        });
                        return true;
                  }

Which ideally (since this is verbatim in a cshtml file) would be modified with something like this;

data: {password: $('#new_password').val(), current: $('#existing_password').val(),confirm: $('#confirm_password').val(),
__RequestVerificationToken:@Html.AntiForgeryValue() }

tl;dr is there a way to interact with the AntiForgeyToken before it's turned into an string of html?


Solution

  • You can use code like this (in for example _Layout.cshtml) to add the AntiForgery header to all Ajax POST requests, or you could adapt it for a specific request. (Code assumes you are using jQuery)

    @functions{
        private static string TokenHeaderValue()
        {
            string cookieToken, formToken;
            AntiForgery.GetTokens(null, out cookieToken, out formToken);
            return cookieToken + ":" + formToken;
        }
    }
    
    <script type="text/javascript">
        $(document).ajaxSend(function (event, jqxhr, settings) {
            if (settings.type == "POST") {   
                jqxhr.setRequestHeader('@ValidateHttpAntiForgeryTokenAttribute.RequestVerificationTokenName',
                                       '@TokenHeaderValue()');
            }
        });
    </script>
    

    On the server side for these Ajax calls, you then want to call the overload of AntiForgery.Validate that takes the cookie and form token, which you would enable by adding this attribute to the action methods called via Ajax (explicitly, or by parent controller, or via a filter)

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class,
                    AllowMultiple = false, Inherited = true)]
    public sealed class ValidateHttpAntiForgeryTokenAttribute 
                                     : FilterAttribute, IAuthorizationFilter
    
    {
        public const string RequestVerificationTokenName = "RequestVerificationToken";
    
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                ValidateRequestHeader(filterContext.HttpContext.Request);
            }
            else
            {
                AntiForgery.Validate();
            }
        }
    
        private static void ValidateRequestHeader(HttpRequestBase request)
        {
            string cookieToken = string.Empty;
            string formToken = string.Empty;
    
            var tokenValue = request.Headers[RequestVerificationTokenName];
    
            if (string.IsNullOrEmpty(tokenValue) == false)
            {
                string[] tokens = tokenValue.Split(':');
    
                if (tokens.Length == 2)
                {
                    cookieToken = tokens[0].Trim();
                    formToken = tokens[1].Trim();
                }
            }
    
            AntiForgery.Validate(cookieToken, formToken);
        }