MVC3 AntiForgeryToken breaks on Ajax login
Asked Answered
R

2

9

ASP.NET MVC's AntiForgeryToken mechanism is based on the current HttpContext.User. It uses that value to construct the token when you call Html.AntiForgeryToken(). Basically it is OK (see an explanation in the last paragraph here) but a problem arises when you log in through an Ajax call.

In my code, when a user logs in, the credentials are sent as a Json object in Ajax (the AntiForgeryToken hidden field value is also sent inside the Json), the server authenticates the user, applies FormsAuthentication.SetAuthCookie(), and returns a Json result which contains some user-specific data. In that way, I can avoid full page refresh upon login.

The problem is that every subsequent Ajax request to the server now fails upon ValidateAntiForgeryTokenAttribute, because it now expects an anti-forgery token that is incompatible with the anti-forgery cookie.

How can I get a valid anti-forgery token to put in the client's hidden field so every Json request after login will succeed?

I tried to get a new hidden-field token manually (using AntiForgery.GetHtml() on the action, extracting the token string itself, returning it to the client in Json and placing it in the anti-forgery hidden field manually in JavaScript) but it does not work - a subsequent Ajax call fails on the ValidateAntiForgeryTokenAttribute on the server. In fact, every call to AntiForgery.GetHtml() (which is essentially what Html.AntiForgeryToken() helper does) produces a different token, which invalidates the previous one.

I also tried to set HttpContext.User = new GenericPrincipal(new GenericIdentity(email), null); as detailed here, but it doesn't work.

Note: This solution doesn't work for me, because of my specific situation: An Ajax login which changes the user identity on the server and hence every token that was generated before the login is invalid; this solution also doesn't apply because it addresses a different problem.

Respiratory answered 17/10, 2011 at 11:39 Comment(5)
Why are you using an AntiForgeryToken on your login page, when the user is unauthenticated. What are you protecting?Exanimate
The login feature is not a page, it's a fragment inside the site's template. It is indeed not needed in the login feature, but the problem arises afterwards - after the login method on the server side sets the current user (HttpContext.User) and returns. At this stage, the page should already have some anti forgery token hidden field, to serve further Ajax calls.Respiratory
Phil Haack posted this article a few days ago. Is this in any way related to your problem? haacked.com/archive/2011/10/10/preventing-csrf-with-ajax.aspx ..."The problem lies in the fact that the under the hood, deep within the call stack, the attribute peeks into the Request.Form collection to grab the anti-forgery token. But when you post JSON encoded data, there is no form collection to speak of."Verb
No Jasper, does not apply to me...Respiratory
Same issue, did you ever find an actual solution to the problem?Succentor
L
6

You will need to clear and redo any existing form token you have upon login. This means your login code will have to either refresh the current page (kinda kills the ajax portion of it eh), your own token implementation, or you will need to refresh your token. It is possible to request a partial view, extract the token, and update your form. You could actually have a restful url which returns nothing but a token to an authenticated user. One may argue this is a security issue, but I don't believe so because it is simply an easier way to get a token rather than requesting any view -partial or otherwise.

You should be able to easily get the token instances to replace via:

var token = $('input[name=""__RequestVerificationToken""]');

EDIT After re-reading a few more times - I question

Why would you have a token on the form if the user isn't logged in. You allow the same form to be 'operated' while not logged in and logged in? Most sites on the net even in this case will redirect for a login. Am I understanding this correctly? If so, you may want to consider skipping the token here or use a second type of token for unauthenticated users. You I believe are saying an unauthenticated user can already submit something in the application - again if I understand this correctly - without being authenticated.

Larval answered 18/10, 2011 at 18:42 Comment(2)
As for your last question, why do I need a token for unauthenticated users, please see my answer to Ben's comment. I'll try to create a partial view with a token and update you with the results. Sounds like a good idea!Respiratory
Didn't get to it yet, but I'll accept your answer because it sounds promising :-)Respiratory
S
3

Ok, what I did was combine the answer from here: jQuery Ajax calls and the Html.AntiForgeryToken() with a partial. I'm using knockout but for those of you not familiar with it you should still be able to follow along pretty easily.

First my html:

<form id="__AjaxAntiForgeryForm" action="#" method="post">@{Html.RenderPartial("AntiForgeryToken");}</form>
<div id="loginTestView">
    <button data-bind="visible: signedIn() == false,click: signIn">Sign In</button>
    <button data-bind="visible: signedIn, click: signOut">Sign Out</button>

    <form>
        <button data-bind="click: testToken">Test Token</button>
    </form>
</div>

The main difference being that instead of @Html.AntiForgeryToken() I have a AntiForgeryToken partial that contain @Html.AntiForgeryToken().

So to really clarify I now have a AntiForgeryToken.cshtml file with just:

@Html.AntiForgeryToken()

Now when you sign in/out you need to update the token so the javascript/jquery looks like:

$(document).ready(function () {
    AddAntiForgeryToken = function (data) {
        data.__RequestVerificationToken = $('#__AjaxAntiForgeryForm input[name=__RequestVerificationToken]').val();
        return data;
    };

    var viewmodel = function () {
        var vm = this;

        vm.signedIn = ko.observable(false);

        vm.signIn = function () {
            $.post('Home/SignIn', function () {
                vm.signedIn(true);
                $.get('Home/GetAuthToken', function (newToken) {
                    $('#__AjaxAntiForgeryForm').html(newToken);
                });
            });

        };
        vm.signOut = function () {
            $.post('Home/SignOut', function () {
                vm.signedIn(false);
                $.get('Home/GetAuthToken', function (newToken) {
                    $('#__AjaxAntiForgeryForm').html(newToken);
                });
            });
        };
        vm.testToken = function () {
            $.post('Home/TestToken', AddAntiForgeryToken({ stuff: 'stuff' }));
        };
    };

    ko.applyBindings(new viewmodel(), $('#loginTestView')[0]);
});

The main thing to pay attention to here is that the $.get needs to happen after the $.post to signIn/Out. This code could be cleaned up a bit, but that's the main take away. If you don't then since the requests are asynchronous the $.get could (and probably will) come back before you are actually signed in.

That should do it. I haven't run into any other times when the token is updated but it would just require just another call to update the partial.

Succentor answered 22/8, 2012 at 17:35 Comment(1)
Nice solution @rball, I'll give it a try (will take some time though).Respiratory

© 2022 - 2024 — McMap. All rights reserved.