Search code examples
jqueryajaxasp.net-mvcpostantiforgerytoken

MVC 5 Multiple Anti Forgery tokens AJAX


My issue is that I have a partial view which holds a form full of drop downs. I am using AJAX to dynamically update drop down content. Everything works fine until I implement the Anti Forgery Tokens. Upon serialising my form in the ajax post, I am picking up both the anti forgery token in my _LoginPartial.cshtml form and the anti forgery token in the form I am posting data from _config.cshtml. This is leading to the validation attribute canning the post and stopping ajax from working.

I have tried the following:

  1. Removing all tokens and just placing a single token in the _Layout.cshtml page and referring to that in the ajax call. This works for Ajax but upon submitting the form I get a token not found error since the token is not in the form.

  2. I then tried posting the form with AJAX too and referring to the single token in the _Layout.cshtml view. This returned data to ajax and not an entire view as I was hoping.

My questions are:

  1. How can I put only the current forms token into the ajax post function?
  2. Alternatively, is there a correct way to do what I am trying to do?

I am pretty certain there is a simple way to sort this out but I am really just a beginner with all this and I am trying to find my feet. Any help would be appreciated.

_LoginPartial.cshtml

@using Microsoft.AspNet.Identity
@using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" }))
{

    if (Request.IsAuthenticated)
    {
        <ul class="nav navbar-nav navbar-right">
            <li>
                @Html.ActionLink("Hello " + User.Identity.GetUserName() + "!", "Index", "Manage", routeValues: null, htmlAttributes: new { title = "Manage" })
            </li>
            @if (User.IsInRole("admin"))
            { 
                <li>@Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
            }
            <li><a href="javascript:document.getElementById('logoutForm').submit()">Log off</a></li>
        </ul>
    }
    else
    {
        <ul class="nav navbar-nav navbar-right">
            @if (User.IsInRole("admin"))
            {
                <li>@Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
            }
            <li>@Html.ActionLink("Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
        </ul>
    }

_Layout.cshtml

 <!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - Doogies Website</title>
    @Styles.Render("~/Content/css")
    @Styles.Render("~/Content/themes/base/css")
    @Scripts.Render("~/bundles/modernizr")
    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/jqueryui")
    @Scripts.Render("~/bundles/jqueryval")
</head>
<body>

    @using (Html.BeginForm(null, null, FormMethod.Post, new { id = "_AFT"})) { @Html.AntiForgeryToken()}

    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Doogies Site", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("Home", "Index", "Home")</li>
                    <li>@Html.ActionLink("About", "About", "Home")</li>
                    <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
                    <li>@Html.ActionLink("RTCG", "RTCG_Configurator", "Rtcg")</li>
                </ul>
                @Html.Partial("_LoginPartial")
            </div>
        </div>
    </div>

    <div class="container body-content" >
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - Chris Duguid</p>
        </footer>
    </div>


    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
</body>
</html>

_config.cshtml (partial view where the form resides)

Yes I am aware the form has no submit information. I am at the point where I was trying out the submitting using ajax.

@model HomeWeb.Models.RtcgConfigurationModel

@using (Html.BeginForm( new { enctype = "multipart/form-data" , id = "configform"}))
{
    <div class="form-horizontal">
        <h3>Configure RTCG</h3>
        <hr />
        @Html.ValidationSummary(true)


        <div class="form-group">
            @Html.LabelFor(model => model.RtcgCabinetType, "Cabinet Type", new { @class = "control-label col-md-5" })
            <div class="col-md-7">
                @Html.DropDownListFor(model => Model.RtcgCabinetType, Model.Cabinets, new { onchange = "changed();" })
                @Html.ValidationMessageFor(model => model.RtcgCabinetType)
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.RtcgAdaptorType, "Adaptor Type", new { @class = "control-label col-md-5" })
            <div class="col-md-7">
                @Html.DropDownListFor(model => Model.RtcgAdaptorType, Model.Adaptors, new { onchange = "changed();" })
                @Html.ValidationMessageFor(model => model.RtcgAdaptorType)
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.RtcgQtyAdaptors, "Adaptor Qty.", new { @class = "control-label col-md-5" })
            <div class="col-md-7">
                @Html.DropDownListFor(model => Model.RtcgQtyAdaptors, Model.AdaptorQtys, new { onchange = "changed();" })
                @Html.ValidationMessageFor(model => model.RtcgQtyAdaptors)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.RtcgTerminationMethod, "Termination", new { @class = "control-label col-md-5" })
            <div class="col-md-7">
                @Html.DropDownListFor(model => Model.RtcgTerminationMethod, Model.TerminationMethods)
                @Html.ValidationMessageFor(model => model.RtcgTerminationMethod)
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.RtcgFaceplateStyle, "Faceplate Style", new { @class = "control-label col-md-5" })
            <div class="col-md-7">
                @Html.DropDownListFor(model => Model.RtcgFaceplateStyle, Model.FacePlateStyles)
                @Html.ValidationMessageFor(model => model.RtcgFaceplateStyle)
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.RtcgScreenPrinting, "Screenprinting", new { @class = "control-label col-md-5" })
            <div class="col-md-7">
                @Html.DropDownListFor(model => Model.RtcgScreenPrinting, Model.ScreenPrintOptions)
                @Html.ValidationMessageFor(model => model.RtcgScreenPrinting)
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.RtcgApplication, "Fibre Type", new { @class = "control-label col-md-5" })
            <div class="col-md-7">
                @Html.DropDownListFor(model => Model.RtcgApplication, Model.Applications)
                @Html.ValidationMessageFor(model => model.RtcgApplication)
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.RtcgVerminProof, "Vermin Proof", new { @class = "control-label col-md-5" })
            <div class="col-md-7">
                @Html.DropDownListFor(model => Model.RtcgVerminProof, Model.VerminProofing)

                @Html.ValidationMessageFor(model => model.RtcgVerminProof)
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-5 col-md-10">
                <input  value="Get Quotation" class="btn btn-primary" style="float: none" onclick="submitajax()" />
            </div>
        </div>
    </div>
}

<script type="text/javascript">
    function changed() {
        var tokensource = $('#_AFT');
        var formData = $('form').serialize();
        var token = $('input[name="__RequestVerificationToken"]', tokensource).val();
        $.extend(formData, { '__RequestVerificationToken': token });
        $.ajax({
           url: '/Rtcg/Ajax_DropdownChanged',
            type: "POST",
            data: formData,
            success: function (result) {
                $('#config-form').html(result);
            },
        })
    }

    function submitajax() {
        var tokensource = $('#_AFT');
        var formData = $('form').serialize();
        var token = $('input[name="__RequestVerificationToken"]', tokensource).val();
        $.extend(formData, { '__RequestVerificationToken': token });
        $.ajax({
            url: '/Rtcg/updateDB',
            type: "POST",
            data: formData,
            success: function (result) {

            },
        })
    }
</script>

RTCG_Configurator ('index' view presented when body rendered)

@model HomeWeb.Models.RtcgConfigurationModel
@{
    ViewBag.Title = "RTCG Configurator";
}

<section >
    <div class="col-md-6 pv" id="config-form">
        @Html.Partial("_config")
    </div>    
    <div class="col-md-6" >
    </div> 
</section>
<div style="clear:both">
    <br /><br />
    <p><a href="~/Home/Index" class="btn btn-primary btn-sm">&laquo; Back Home </a></p>
</div>

Controller - Ajax_DropdownChanged()

// POST: handle dropdown changes from Ajax
      [HttpPost]
      [ValidateAntiForgeryToken]
        public ActionResult Ajax_DropdownChanged(RtcgConfigurationModel formData)
        {  
            //handle 'postback' displaying the fields. 
            if (ModelState.IsValid)
            { 
                RtcgConfigurationModel model = new RtcgConfigurationModel();
                using (var db = new ApplicationDbContext())
                {
                    model.Cabinets = DisplayElement.Cabinets(formData);
                    model.Adaptors = DisplayElement.Adaptors(formData);
                    model.AdaptorQtys = DisplayElement.AdaptorQuantities(formData);
                    model.TerminationMethods = DisplayElement.Termination(formData);
                    model.FacePlateStyles = DisplayElement.Faceplates(formData);
                    model.ScreenPrintOptions = DisplayElement.ScreenPrinting(formData);
                    model.Applications = DisplayElement.Application(formData);
                    model.VerminProofing = DisplayElement.VerminProof(formData);   
                }
                return PartialView("_config", model);
                //return View(model);
            }
            else
            {
                return PartialView();
            }
        }

Solution

  • Your use of var formData = $('form').serialize(); will serialize the values of all form controls in all forms in the view, including the antiforgery tokens, which is why you submitting multiple tokens.

    Ensure each form has an id attribute, and then to submit only the values (and the token) in the form with id="configform", use

    var formData = $('#configform').serialize();
    

    Note also that you do not need

    var token = $('input[name="__RequestVerificationToken"]', tokensource).val();
    $.extend(formData, { '__RequestVerificationToken': token });
    

    since .serialize() already includes the name/value pair for the token (assuming @Html.AntiForgeryToken() is included between the <form> tags)