Reload AntiForgeryToken after a login
Asked Answered
B

2

10

I need to reload an AntiForgeryToken in a form located in a view, after a successfull login in another view in the same page.

Can I make an update in the form input @Html.AntiForgeryToken() key with the new one from the result login page via jQuery?

If yes, is this recomended and secure?

How can I do it?

EDIT:

In the Layout I have diferent PartialViews:

The Partial to login:

<ul class="menudrt" id="headerLogin">
    @{ Html.RenderAction(MVC.Account.LoginHeader()); }
</ul>

And in another Partial, the hability to send a comment:

<div class="comentsform">

    <!-- Comments form -->
    @{ Html.RenderAction(MVC.Comment.Create()); }

</div>

To send a comment, the user have to login, so after login, the comment form needs to update the AntiForgeryToken or I get the validation error because it's diferent now that the login has been made.

Thanks

Blanchblancha answered 29/5, 2013 at 13:45 Comment(0)
R
24

The issue is occurring because the AntiForgery token contains the username of the currently authenticated user.

So here's what happens:

  1. An anonymous user navigates to your page
  2. An antiforgery token is generated for the comment form but this token contains an empty username (because at that moment the user is anonymous)
  3. You are using an AJAX call to login
  4. The user submits the comment form to the server and the token validation fails because the empty username contained in the initial token is different than the currently authenticated username.

So you have a couple of options to fix this issue:

  1. At step 3. do not use an AJAX call. Use a standard form submit to login the user and redirect him back to the initially requested page. The comment form will of course be reloaded and correct antiforgery token generated for it.
  2. Refresh the antiforgery token after logging-in

The obviousness of solution 1. doesn't make it a good candidate for covering it in my answer. Let's see how the second solution could be implemented.

But first let's reproduce the problem with an example:

Controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Login()
    {
        FormsAuthentication.SetAuthCookie("john", false);
        return Json(new { success = true });
    }

    [HttpPost]
    [ValidateAntiForgeryToken()]
    public ActionResult Comment()
    {
        return Content("Thanks for commenting");
    }
}

~/Views/Home/Index.cshtml:

<div>
    @{ Html.RenderPartial("_Login"); }
</div>

<div id="comment">
    @{ Html.RenderPartial("_Comment"); }
</div>

<script type="text/javascript">
    $('#loginForm').submit(function () {
        $.ajax({
            url: this.action,
            type: this.method,
            data: $(this).serialize(),
            success: function (result) {
                alert('You are now successfully logged in');
            }
        });
        return false;
    });
</script>

~/Views/Home/_Login.cshtml:

@using (Html.BeginForm("Login", null, FormMethod.Post, new { id = "loginForm" }))
{
    @Html.AntiForgeryToken()
    <button type="submit">Login</button>
}

~/Views/Home/_Comment.cshtml:

@using (Html.BeginForm("Comment", null, FormMethod.Post))
{ 
    @Html.AntiForgeryToken()
    <button type="submit">Comment</button>
}

Alright now when you navigate to the Home/Index the corresponding view will be rendered and if you press on the Comment button without logging-in first it will work. But if you login and then Comment it will fail.

So we could add another controller action that will return a partial view with a simple Html.AntiForgeryToken call in order to generate a fresh token:

public ActionResult RefreshToken()
{
    return PartialView("_AntiForgeryToken");
}

and the corresponding partial (~/Views/Home/_AntiForgeryToken.cshtml):

@Html.AntiForgeryToken()

And the final step is to refresh the token by updating our AJAX call:

<script type="text/javascript">
    $('#loginForm').submit(function () {
        $.ajax({
            url: this.action,
            type: this.method,
            data: $(this).serialize(),
            success: function (result) {
                $.get('@Url.Action("RefreshToken")', function (html) {
                    var tokenValue = $('<div />').html(html).find('input[type="hidden"]').val();
                    $('#comment input[type="hidden"]').val(tokenValue);
                    alert('You are now successfully logged in and can comment');
                });
            }
        });
        return false;
    });
</script>
Ranson answered 7/6, 2013 at 6:26 Comment(8)
Hi, thanks. And can I copy the token that I get from the login token result to the comment token form ?Blanchblancha
No, there needs to be an additional AJAX call, because at the moment you are calling the Login action there's still not a forms authentication cookie present in the request and the token generated in this action will be invalid. The principal will be set only on subsequent request.Ranson
The issue here is that login is made in a div, and after the login is done, I update the code with an Ajax.BeginForm/ UpdateTargetId/ InsertionMode with the new AntiForgeryToken to garanty no refresh at all in the website.Blanchblancha
That's what my example illustrates. An AJAX call to the Login action (in your case that would be an Ajax.BeginForm) and in the success callback (in your case that would be the OnSuccess AjaxOption) send another AJAX request to refresh the token. I can't make it any more clear than that.Ranson
Hi, I have use your jquery code to refresh the token to every forms in the page and it worked fine. I don't know why but the bounty was set to another user, but I have set your answer as correct because you have answered sooner. Thank you again for your help.Blanchblancha
@Patrick, the reason why the bounty was awarded to another answer is because you didn't explicitly award the bounty to the answer that helped you solve the problem. In this case the bounty (half of it) is automatically awarded by the system to answer with most votes.Ranson
Sorry I didn't know. I will be careful next time. Regards.Blanchblancha
Thanks . Nice Answer below code work for me * var tokenValue = $('<div />').html(html).find('input[type="hidden"]').val();*Klaus
V
7

You can achieve this by simply returning the AntiForgeryToken after they log in.

No need to re-use the same token 2 times.

Controller:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model)
{
  // do something with login
  // return new token as a partial to parse and get value
  return this.PartialView("_AntiForgeryPartial");
}

_AntiForgeryPartial:

@Html.AntiForgeryToken()

You can use JS similar to this to load ONLY the new AntiForgeryToken value into the comment form.

View:

$("#LoginForm").submit(function (e) {
    e.preventDefault();

    var $this = $(this);

    $.ajax({
        type: $this.attr("method"),
        url: $this.attr("action"),
        data: $this.serialize(),
        success: function (response) {
            // get the new token from the response html
            var val = $(response).find('input[type="hidden"]').val();
            // set the new token value
            $('.commentsform input[type="hidden"]').val(val);
        }
    });
});

When the comment form does the POST, you should be able to validate against the new unique AntiForgeryToken.

Steven Sanderson has a great post on the AntiForgeryToken() if you would like to learn more on how to use it and what it's for.

Visible answered 6/6, 2013 at 19:14 Comment(6)
HI, thanks but as soon you login and change the associated user to the token, any other submit is no longer valid.Blanchblancha
@Blanchblancha I've given examples of how to achieve this in my answer now.Visible
Hi thanks, but the place where is the other form with the AntiForgeryToken can't be refreshed, I need to update it without refreshing the Partialview.Blanchblancha
@Blanchblancha I've updated once again SLIGHTLY changing things to work for your requirement of not refreshing the comment form. Please try my new answer. :)Visible
@Blanchblancha I see that I got the bounty for my answer. Is this the correct answer to the question?Visible
Hi, in part yes but it was not me to set the bounty. I will set the right answer to Darin because it has also helped me and he has answered to the question sooner. Thank you again for your help.Blanchblancha

© 2022 - 2024 — McMap. All rights reserved.