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?
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.
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.
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.
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.
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 !
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.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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With