3

The CSRF prevention support in a django application sends CSRF tokens down to a client via a cookie, and accepts CSRF tokens from the client in either a header (X-CSRFToken) or a cookie. This works fine for non-CORS, non-AJAX web applications. But it doesn't appear to work if you a) have a single page web app that communicates with the server via AJAX, and b) the single page webapp is hosted in a different domain than the server (CORS).

The issue is that the single page webapp (from domain1) cannot read the server domain (domain2) cookies using xhr.getResponseHeader or getCookie due to CORS restrictions. How can the javascript webapp send the appropriate CSRF token to the server given that it can't read the cookies?

the xhr.getResponseHeader api is restricted from retrieving the Set-Cookie or Set-Cookie2 headers (by spec) and the various CORS-supporting browsers appear to enforce this restriction. Similarly, the getCookie JS function will read all non-httpOnly cookies in the webapp domain (domain1), but will not read the ones set by the server in its domain (domain2).

This is not a problem in non-CORS cases, but in our application, we wish to host the API in a different domain than the client webapp. Any suggestions?

fab
  • 317
  • 4
  • 20
eswenson
  • 745
  • 1
  • 9
  • 25

1 Answers1

0

I think I was experiencing the same problem and resolved it through a combination of server-side stuff and client-side stuff. Server-side (Django-Python):

origin = request.META.get('HTTP_ORIGIN', None)
if origin and origin in settings.safe_origins:
    response['Access-Control-Allow-Origin'] = origin
    response['Access-Control-Allow-Credentials'] = 'true'
    response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
    response['Access-Control-Allow-Headers'] = request.META.get('Access-Control-Allow-Headers', 'x-requested-with, X-CSRFToken',)
    response['Access-Control-Max-Age'] = 15
    response['Allow'] = 'GET, POST, PUT, DELETE, OPTIONS'

Client-side on startup:

$.ajaxPrefilter(function(options, originalOptions, jqXHR)
{
    options.crossDomain =
    {
        crossDomain: true
    };
    options.xhrFields =
    {
        withCredentials: true
    };
});

function csrfSafeMethod(method)
{
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}

$.ajaxSetup(
{
    crossDomain: false, // obviates need for sameOrigin test
    beforeSend: function(xhr, settings)
    {
        if (!csrfSafeMethod(settings.type))
        {
            xhr.setRequestHeader("X-CSRFToken", csrf);
        }
    }
});

And then for good measure in the $.ajax calls themselves:

$.ajax(
{
    type: "POST",
    url: theUrl,
    data: theData,
    contentType: 'application/x-www-form-urlencoded',
    dataType: 'json',
    xhrFields:
    {
       withCredentials: true
    }
});

For me, the thing I was missing and hitting my head against the wall over was this part:

xhrFields:
{
   withCredentials: true
}

Hope this helps someone.

gcdev
  • 1,406
  • 3
  • 17
  • 30
  • Interesting. I believe we're doing everything you have described above, except setting crossDomain: false in the ajax requests. I think we have explicitly set crossDomain: true. I'll look into this. Thanks. – eswenson Mar 30 '14 at 17:30