Jay
Jay

Reputation: 751

MVC - anti-forgery token error

I've been brought onto my first MVC and C# project so I appreciate any guidance.

I've created a new feature that checks to see if user has had security training when they log in. If they haven't, the user is directed to a training page where they simply agree/disagree to the rules. If the user agrees, login is completed. If user disagrees, he/she is logged off.

The issue that I have is that when I select the Agree/Disagree button in the training view, I get the following error It should route me to the homepage or logout the user.

Controller

 public ActionResult UserSecurityTraining(int ID, string returnUrl)
    {
        // check if user already has taken training (e.g., is UserInfoID in UserSecurityTrainings table)
        var accountUser = db.UserSecurityTraining.Where(x => x.UserInfoID == ID).Count();
        // If user ID is not in UserSecurityTraining table...
        if (accountUser == 0)
    {
        // prompt security training for user
        return View("UserSecurityTraining");
    }
    // If user in UserSecurityTraining table...
    if (accountUser > 0)
    {
        return RedirectToLocal(returnUrl);
    }
    return View();
}

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> UserSecurityTrainingConfirm(FormCollection form, UserSecurityTraining model)
{
    if (ModelState.IsValid)
    {
        if (form["accept"] != null)
        {
            try
            {
                // if success button selected
                //UserSecurityTraining user = db.UserSecurityTraining.Find(); //Create model object
                //var user = new UserSecurityTraining { ID = 1, UserInfoID = 1, CreatedDt = 1 };

                logger.Info("User has successfully completed training" + model.UserInfoID);
                model.CreatedDt = DateTime.Now;
                db.SaveChanges();
                //return RedirectToAction("ChangePassword", "Manage");
            }
            catch (Exception e)
            {
                throw e;
            }
            return View("SecurityTrainingSuccess");
        }
        if(form["reject"] != null)
        {
            return RedirectToAction("Logoff", "Account");
        }
    }
    return View("UserSecurityTraining");
}

View

@model ECHO.Models.UserSecurityTraining
@{
    ViewBag.Title = "Security Training";
    Layout = "~/Views/Shared/_LayoutNoSidebar.cshtml";
}

<!--<script src="~/Scripts/RequestAccess.js"></script>-->
<div class="container body-content">
    <h2>@ViewBag.Title</h2>
    <div class="row">
        <div class="col-md-8">
            @using (Html.BeginForm("UserSecurityTrainingConfirm", "Account", FormMethod.Post, new { role = "form" }))
            {
                <fieldset>
                    @Html.AntiForgeryToken()
                    Please view the following security training slides:<br><br>
                    [INSERT LINK TO SLIDES]<br><br>
                    Do you attest that you viewed, understood, and promise to follow the guidelines outlined in the security training?<br><br>
                    <input type="submit" id="accept" class="btn btn-default" value="Accept" />
                    <input type="submit" id="reject" class="btn btn-default" value="Reject" />

                </fieldset>
            }                
        </div><!--end col-md-8-->
    </div><!--end row-->
</div><!-- end container -->

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Upvotes: 0

Views: 1564

Answers (1)

Chris Pratt
Chris Pratt

Reputation: 239460

I don't think you've provided enough of your code to properly diagnose this particular error. In general, this anti-forgery exception is due to a change in authentication status. When you call @Html.AntiForgeryToken() on a page, a cookie is set as well in the response with the token. Importantly, if the user is authenticated, then the user's identity is used to compose that token. Then, if that user's authentication status changes between when the cookie is set and when the form is posted to an action that validates that token, the token will no longer match. This can either be the user being authenticated after the cookie set or being logged out after the cookie is set. In other words, if the user is anonymous when the page loads, but becomes logged in before submitting the form, then it will still fail.

Again, I'm not seeing any code here that would obviously give rise to that situation, but we also don't have the full picture in terms of how the user is being logged in an taken to this view in the first place.

That said, there are some very clear errors that may or may not be causing this particular issue, but definitely will cause an issue at some point. First, your buttons do not have name attributes. Based on your action code, it appears as if you believe the id attribute will be what appears in your FormCollection, but that is not the case. You need to add name="accept" and name="reject", respectively, to the buttons for your current code to function.

Second, on the user successfully accepting, you should redirect to an action that loads the SecurityTrainingSuccess view, rather than return that view directly. This part of the PRG (Post-Redirect-Get) pattern and ensures that the submit is not replayed. Any and all post actions should redirect on success.

Third, at least out of the box, LogOff is going to be a post action, which means you can't redirect to it. Redirects are always via GET. You can technically make LogOff respond to GET as well as or instead of POST, but that's an anti-pattern. Atomic actions should always be handled by POST (or a more appropriate verb like PUT, DELETE, etc.), but never GET.

Finally, though minor, the use of FormCollection, in general, is discouraged. For a simple form like this, you can literally just bind your post as params:

public ActionResult UserSecurityTrainingConfirm(string accept, string reject, ...)

However, then it'd probably be more logical and foolproof to introduce a single boolean like:

public ActionResult UserSecurityTrainingConfirm(bool accepted, ...)

Then, your buttons could simply be:

    <button type="submit" name="accepted" value="true" class="btn btn-default">Accept</button>
    <button type="submit" name="accepted" value="false" class="btn btn-default">Reject</button>

This essentially makes them like radios. The one that is clicked submits its value, so the accepted param, then, will be true or false accordingly. Also, notice that I switched you to true button elements. The use of input for buttons is a bad practice, especially when you actually need it to submit a value, since the value and the display are inherently tied together. With a button element, you can post whatever you want and still have it labeled with whatever text you want, independently.

Upvotes: 2

Related Questions