The issue is occurring because the AntiForgery token contains the username of the currently authenticated user.
So here's what happens:
- An anonymous user navigates to your page
- An antiforgery token is generated for the comment form but this token contains an empty username (because at that moment the user is anonymous)
- You are using an AJAX call to login
- 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:
- 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.
- 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>