Sunday, June 10, 2012

Django CSRF check failing with an Ajax POST request


I could use some help complying with Django's csrf protection mechanism via my AJAX post. I've followed the directions here:



http://docs.djangoproject.com/en/dev/ref/contrib/csrf/



I've copied the AJAX sample code they have on that page exactly:



http://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax



I put an alert printing the contents of getCookie('csrftoken') before the xhr.setRequestHeader call and it is indeed populated with some data. I'm not sure how to verify that the token is correct, but I'm encouraged that it's finding and sending something.



But django is still rejecting my AJAX post.



Here's my Javascript:




$.post("/memorize/", data, function (result) {
if (result != "failure") {
get_random_card();
}
else {
alert("Failed to save card data.");
}
});



Here's the error I'm seeing from Django:



[23/Feb/2011 22:08:29] "POST /memorize/ HTTP/1.1" 403 2332



I'm sure I'm missing something, and maybe it's simple, but I don't know what it is. I've searched around SO and saw some information about turning off the CSRF check for my view via the csrf_exempt decorator, but I find that unappealing. I've tried that out and it works, but I'd rather get my POST to work the way Django was designed to expect it, if possible.



Just in case it's helpful, here's the gist of what my view is doing:




def myview(request):

profile = request.user.profile

if request.method == 'POST':
"""
Process the post...
"""
return HttpResponseRedirect('/memorize/')
else: # request.method == 'GET'

ajax = request.GET.has_key('ajax')

"""
Some irrelevent code...
"""

if ajax:
response = HttpResponse()
profile.get_stack_json(response)
return response
else:
"""
Get data to send along with the content of the page.
"""

return render_to_response('memorize/memorize.html',
""" My data """
context_instance=RequestContext(request))



Thanks for your replies!


Source: Tips4all

8 comments:

  1. Real solution

    Ok, I managed to trace the problem down. It lies in the Javascript (as I suggested below) code.

    What you need is this:

    $.ajaxSetup({
    beforeSend: function(xhr, settings) {
    function getCookie(name) {
    var cookieValue = null;
    if (document.cookie && document.cookie != '') {
    var cookies = document.cookie.split(';');
    for (var i = 0; i < cookies.length; i++) {
    var cookie = jQuery.trim(cookies[i]);
    // Does this cookie string begin with the name we want?
    if (cookie.substring(0, name.length + 1) == (name + '=')) {
    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
    break;
    }
    }
    }
    return cookieValue;
    }
    if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
    // Only send the token to relative URLs i.e. locally.
    xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
    }
    }
    });


    instead of the code posted in the official docs:
    http://docs.djangoproject.com/en/1.2/ref/contrib/csrf/#ajax

    The working code, comes from this Django entry: http://www.djangoproject.com/weblog/2011/feb/08/security/

    So the general solution is: "use ajaxSetup handler instead of ajaxSend handler". I don't know why it works. But it works for me :)

    Previous post (without answer)

    I'm experiencing the same problem actually.

    It occurs after updating to Django 1.2.5 - there were no errors with AJAX POST requests in Django 1.2.4 (AJAX wasn't protected in any way, but it worked just fine).

    Just like OP, I have tried the JavaScript snippet posted in Django documentation. I'm using jQuery 1.5. I'm also using the "django.middleware.csrf.CsrfViewMiddleware" middleware.

    I tried to follow the the middleware code and I know that it fails on this:

    request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '')


    and then

    if request_csrf_token != csrf_token:
    return self._reject(request, REASON_BAD_TOKEN)


    this "if" is true, because "request_csrf_token" is empty.

    Basically it means that the header is NOT set. So is there anything wrong with this JS line:

    xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));


    ?

    I hope that provided details will help us in resolving the issue :)

    ReplyDelete
  2. If you use the $.ajax, you can simply add the csrf token in the data body:

    $.ajax({
    data: {
    somedata: 'somedata',
    moredata: 'moredata',
    csrfmiddlewaretoken: '{{ csrf_token }}'
    },

    ReplyDelete
  3. add this line to you're jQuery code:

    $.ajaxSetup({
    data: {csrfmiddlewaretoken: '{{ csrf_token }}' },
    });


    and done.

    ReplyDelete
  4. Use Firefox with Firebug. Open the 'Console' tab while firing the ajax request. With DEBUG=True you get the nice django error page as response and you can even see the rendered html of the ajax response in the console tab.

    Then you will know what the error is.

    ReplyDelete
  5. The issue is because django is expecting the value from the cookie to be passed back as part of the form data. The code from the previous answer is getting javascript to hunt out the cookie value and put it into the form data. Thats a lovely way of doing it from a technical point of view, but it does look a bit verbose.

    In the past, I have done it more simply by getting the javascript to put the token value into the post data.

    If you use {% csrf_token %} in your template, you will get a hidden form field emitted that carries the value. But, if you use {{ csrf_token }} you will just get the bare value of the token, so you can use this in javascript like this....

    csrf_token = "{{ csrf_token }}";


    Then you can include that, with the required key name in the hash you then submit as the data to the ajax call.

    ReplyDelete
  6. I've just encountered a bit different but similar situation. Not 100% sure if it'd be a resolution to your case, but I resolved the issue for Django 1.3 by setting a POST parameter 'csrfmiddlewaretoken' with the proper cookie value string which is usually returned within the form of your home HTML by Django's template system with '{% csrf_token %}' tag. I did not try on the older Django, just happened and resolved on Django1.3.
    My problem was that the first request submitted via Ajax from a form was successfully done but the second attempt from the exact same from failed, resulted in 403 state even though the header 'X-CSRFToken' is correctly placed with the CSRF token value as well as in the case of the first attempt.
    Hope this helps.

    Regards,

    Hiro

    ReplyDelete
  7. The accepted answer is most likely a red herring. The difference between Django 1.2.4 and 1.2.5 was the requirement for a CSRF token for AJAX requests.

    I came across this problem on Django 1.3 and it was caused by the CSRF cookie not being set in the first place. Django will not set the cookie unless it has to. So an exclusively or heavily ajax site running on Django 1.2.4 would potentially never have sent a token to the client and then the upgrade requiring the token would cause the 403 errors.

    The ideal fix is here:
    http://docs.djangoproject.com/en/dev/ref/contrib/csrf/#page-uses-ajax-without-any-html-form
    but you'd have to wait for 1.4 unless this is just documentation catching up with the code

    Edit

    Note also that the later Django docs note a bug in jQuery 1.5 so ensure you are using 1.5.1 or later with the Django suggested code:
    http://docs.djangoproject.com/en/1.3/ref/contrib/csrf/#ajax

    ReplyDelete
  8. If your form posts correctly in Django without JS, you should be able to progressively enhance it with ajax without any hacking or messy passing of the csrf token. Just serialize the whole form and that will automatically pick up all your form fields including the hidden csrf field:

    $('#myForm').submit(function(){
    var action = $(this).attr('action');
    var that = $(this);
    $.ajax({
    url: action,
    type: 'POST',
    data: that.serialize()
    ,success: function(data){
    console.log('Success!');
    }
    });
    return false;
    });


    I've tested this with Django 1.3+ and jQuery 1.5+. Obviously this will work for any HTML form, not just Django apps.

    ReplyDelete