Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

django session_key different on each ajax call

I've got a django application that I am accessing only over AJAX. My main problem is I want to get a unique ID which pairs to a particular browser instance making the request.

To try to do this, I'm trying to access the session_key that django creates, but it's sometimes coming back as None.

Here's how I'm creating the JSON response in django:

def get(self, request, pk, format=None):
    resp_obj = {}
    ...
    resp_obj['csrftoken'] = csrftoken
    # shouldn't need the next two lines, but request.session.session_key is None sometimes
    if not request.session.exists(request.session.session_key):
        request.session.create()
    resp_obj['sessionid'] = request.session.session_key
    return JSONResponse(resp_obj)

When I make the request using Postman, the session_key comes through in both the JSON body and in the cookie, but when I make the request through jquery in a browser, request.session.session_key is None, which is why I added these lines:

if not request.session.exists(request.session.session_key):
    request.session.create()

But when I do that, the session_key is different each time.

Here's how I'm making the AJAX call:

for (var i = 0; i < this.survey_ids.length; i++) {
  $.ajax({
    url: this.SERVER_URL+ '/surveys/' + this.survey_ids[i] + '/?language=' + VTA.user_language,
    headers: {
      'Accept-Language': user_language
    }
  }).error(function (jqXHR, textStatus, errorThrown) {
    // handle the error
  }).done(function (response, textStatus, jqXHR) {

    window.console.log(response.csrftoken)  // different on each iteration
    window.console.log(response.sessionid)  // also different on each iteration

    //handle response

  })
}

The Django documentation says that sessions are not always created:

By default, Django only saves to the session database when the session has been modified – that is if any of its dictionary values have been assigned or deleted

https://docs.djangoproject.com/en/1.9/topics/http/sessions/#when-sessions-are-saved

Is there a way to force django session_key creation even when is session not modified, but not have it change when it shouldn't? Or is there a way to "modify the session" such that it gets created properly like Postman is doing?

like image 787
Kyle Falconer Avatar asked Feb 07 '23 20:02

Kyle Falconer


1 Answers

Your problem basically comes from the cross-domain part of your request. I copied your example, and tried by accessing the main page with localhost, then sending the Ajax request on localhost as well, and the session key is conserved.

However, when I change the Ajax request to be on 127.0.0.1 (and with a proper configuration of django-cors-headers), I can get the exact same issue as what you describe. The session key is changed for each request. (The CSRF token as well, but this is on purpose, and should be kept this way.)

Here, you have a mix of CORS, third-party cookies, and credentials in XMLHttpRequest which makes the whole thing break.

What exactly is happening?

Context

First, let's agree on the content of your files.

# views.py
from django.http import JsonResponse
from django.middleware import csrf

def ajax_call(request):
    if not request.session.exists(request.session.session_key):
        request.session.create()
    # To debug the server side
    print request.session.session_key
    print csrf.get_token(request)

    # To debug the client side
    resp_obj = {}
    resp_obj['sessionid'] = request.session.session_key
    resp_obj['csrf'] = csrf.get_token(request)
    return JsonResponse(resp_obj)

The Javascript code included in a larger HTML page:

# Javascript, client side
function call () {
    $.ajax({
        type: 'GET',
        xhrFields: {
            withCredentials: true
        },
        url: 'http://127.0.0.1/ajax_call/'
    }).fail(function () {
        console.log("Error");
    }).done(function (response, textStatus, jqXHR) {
        console.log("Success");
        console.log(response.sessionid);
        console.log(response.csrf);
    })
}

Note the Ajax request is made on 127.0.0.1, and not on localhost, as it is in your case. (Or maybe, you access the main HTML page on 127.0.0.1 and use localhost in the Ajax call, but the idea is the same.)

Finally, you allow CORS in settings.py by adding the adequate lines in INSTALLED_APPS and MIDDLEWARE (see django-cors-headers for more info on the installation). To make it easy, you allow all URLs, and all ORIGIN by adding these lines:

# settings.py
CORS_ORIGIN_ALLOW_ALL = True
CORS_URLS_REGEX = True

Do not copy these lines on a production server if you do not understand what it does! It can open a sever security breech.

Mechanisms

  1. Your browser sends a GET request to the main page (let's imagine it is the root of the website, and the request is GET http://localhost)

Here, it does not really matter if a session is open between your browser and localhost, as the state probably won't change.

The server sends back the HTML page with the Javascript code included.

  1. Your browser interprets the Javascript code. It creates an XMLHttpRequest, which is sent to the server at the address http://127.0.0.1/ajax_call. If a session was previously open with localhost, it cannot be used here, because the domain name is not the same. Anyway, it does not matter, as the session can be created when answering this request.

  2. The server receives a request for 127.0.0.1. If you did not add this host as an allowed-host in your settings.py and you run your server in production mode, an exception is raised. End of the story. If you run in DEBUG mode, the server creates a new session, as none was sent by the client. Everything goes as expected, a new session_key and a new CSRF token are created, and sent to the client in the header. Let's look at that more precisely.

Here is an example of the header sent by the browser:

Host: localhost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost/
X-Requested-With: XMLHttpRequest
Cookie: csrftoken=XCadvu4MJjDMzCkOTTC276oJs9P0j989CBw6rnidz7cS34PoOt1VftqWMqd8BHMX; django_language=en
DNT: 1
Connection: keep-alive

Look closely at the Cookie line. No sessionid is sent, because no session is open.

The header sent back by the server looks like that, if everything went well.

Content-Length: 125
Content-Type: application/json
Date: Sun, 17 Sep 2017 14:21:23 GMT
Server: WSGIServer/0.1 Python/2.7.14rc1
Set-Cookie: csrftoken=XCadvu4MJjDMzCkOTTC276oJs9P0j989CBw6rnidz7cS34PoOt1VftqWMqd8BHMX; expires=Sun, 16-Sep-2018 14:21:22 GMT; Max-Age=31449600; Path=/
sessionid=l505q4y8pywe9t3q76204662a1225scx; expires=Sun, 01-Oct-2017 14:21:22 GMT; httponly; Max-Age=1209600; Path=/
Vary: Cookie
x-frame-options: SAMEORIGIN

The server created a session, and sent all the needed information to the client, as a Set-Cookie header. Looks good !

  1. The behavior we expect from the browser is that it sets the cookie for the domain 127.0.0.1, and uses it for the next request on the same domain. However, looking at a new request header shows that it does not sent the sessionid in the header, making the server create a new session, send the new information as a Set-Cookie header, and start over the process. If you look at the stored cookies on your computer (with Firefox, right click on the page > View Page Info, then Security tab, click on View Cookies), you can see some cookies for localhost, but nothing for 127.0.0.1. The browser does not set the cookie that it gets from the Ajax call.

Why?

For so many reason! We'll list them all on the Solution section. Because the easiest and best way to solve that is not to unlock it, but to use it correctly.

How to solve it?

  1. Here is the first, best, easiest, recommended solution: make your main domain and Ajax call domain match. You will avoid the CORS headache, avoid opening a potential security breech, do not handle third-party cookies, and make your development environment consistent with your production.

  2. If you really want to handle two different domains, ask yourself if you need it. If you don't, go back to 1. If you really really need it, let's check the solution.

    • First, be sure that your browser accepts third-party cookies. For Firefox, in Preferences, go to Privacy tab. In the History section, select Firefox will: Use custom settings for history, and check if Accept third-party cookies is set as Always. If a visitor disables third-party cookies, your site will be broken for him. (First good reason to go to solution 1.)
    • Second, you have to tell your browser not to ignore the Cookie setting when receiving the reply from the Ajax call. Adding withCredentials setting to your xhr is the solution. Here is the new Javascript code:

      function call () {
          $.ajax({
              type: 'GET',
              xhrFields: {
                  withCredentials: true
              },
              url: 'http://127.0.0.1/ajax_call/'
          }). [...]
      
    • If you try right now, you'll see the browser is still not happy. The reason is

      Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ‘http://127.0.0.1/ajax_call/’. (Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’).

      This is a security feature. With this configuration, you open again the security breech which has been blocked by the CORS protection. You allow any website to send an authenticated CORS request. Never do that. Using CORS_ORIGIN_WHITELIST instead of CORS_ORIGIN_ALLOW_ALL fixes this issue.

    • Still not done. Your browser now complains for something else:

      Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://127.0.0.1:8000/ajax_call/. (Reason: expected ‘true’ in CORS header ‘Access-Control-Allow-Credentials’).

      Again, this is a security feature. Sending credentials through a CORS request is most of the time really bad. You have to manually allow it on the resource side. You can do it by activating the CORS_ALLOW_CREDENTIALS setting. Your settings.py file now looks like this:

      CORS_URLS_REGEX = r'.*'
      CORS_ORIGIN_WHITELIST = (
          'localhost',
          '127.0.0.1',
      )
      CORS_ALLOW_CREDENTIALS = True
      
    • Now, you can try again, see that it works, and ask yourself again if you really need to have a different domain in your Ajax call.

like image 80
Dunatotatos Avatar answered Feb 18 '23 19:02

Dunatotatos