Django 3.0.6, Channels 2.4, Channels-Redis 2.4
I'm developing on WSL/Ubuntu 18.04 using runserver and channels_redis
I am writing a chat application, and the whole app is working fine except for this problem. I assign a random username to non-authenticated users. I save this username to self.scope['session'] within the consumer. For authenticated users, the csrftoken cookie and the sessionid cookie are being set correctly in the browser. However, even though I both set and save a scope['session']['user_name'] variable for non-authenticated users in the consumer's connect method, the session is not being saved in the django_session table and the user_name cookie is therefore not being set in the browser. I am using an async consumer, and I am using the correct database_sync_to_async syntax to save the session as shown at the bottom of the first example code shown here.
Relevant code sections:
consumers.py:
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from channels.db import database_sync_to_async
from chat.models import *
class ChatConsumer(AsyncJsonWebsocketConsumer):
DIGITS = string.digits
ANON_NUMBER_LENGTH = 7
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
if not self.scope["user"].is_authenticated:
if 'user_name' not in self.scope['session']:
# Create user name session for anonymous user
random_number = ''.join(random.choice(ChatConsumer.DIGITS) for i in range(ChatConsumer.ANON_NUMBER_LENGTH))
self.scope['session']['user_name'] = f'Guest_{random_number}'
await database_sync_to_async(self.scope["session"].save)()
elif 'user_name' in self.scope['session']:
# Remove user_name from session if present
self.scope['session'].pop('user_name')
await database_sync_to_async(self.scope["session"].save)()
await self.accept()
routing.py:
from channels.auth import AuthMiddlewareStack
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
The above await database_sync_to_async(self.scope["session"].save)() statement is apparently being executed without error (a print statement just before it printed fine). I even tried setting SESSION_SAVE_EVERY_REQUEST to True as noted here (though I didn't expect that to help), but the session is still not persisted. Any suggestions?
So channels does not support setting cookies in websocket consumers... :( there is some open debate on even is browsers will respect cookies on websocket upgrade responses.
however if you ensure that the user already has a session (even if it is anonimuse) before they hit the websocket endpoint then you should (if your using a db backed session) be able to update it at any point (but you will not be able to set any cookies on the clients side).
I have a SPA built using django channels and Vue for frontend.
The very first time user loads the webpage, the index.html is served using
url(r'^.*$', TemplateView.as_view(template_name='index.html'), name="app")
Then frontend communicates with the server using web socket.
One of the messages sent could be to login into the system with appropriate credentials.
I have been following instructions at https://channels.readthedocs.io/en/latest/topics/authentication.html to implement login through the consumer.
class MyConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
self.group_name = str(uuid.uuid1())
print(self.scope['session'].session_key)
self.user = self.scope["user"]
# Join room group
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
# User data
await self.send(simplejson.dumps(dict(data=dict(socketid=self.group_name, user=dict(username=self.user.username or None)))))
async def receive_json(self, jsdata):
kwargs = jsdata.get('kwargs', {})
# Manage Login
if is_login_message(jsdata): # This checks if the message is sent to do login
user = authenticate(**kwargs)
await login(self.scope, user)
print('Saving login session')
await database_sync_to_async(self.scope["session"].save)()
await self.send(simplejson.dumps(dict(data=dict(user=dict(username=user.username)))))
print(self.scope['session'].session_key)
return
Everything works fine. I could see the seesion key being printed when user logs in. However, when I reload the web page, the session is not retained. It prints None in the connect method. I see the cookies are not getting set after the login is done. I expect the information that is sent from the server to client when the below line runs to set some cookies in the browser.
await database_sync_to_async(self.scope["session"].save)()
await self.send(simplejson.dumps(dict(data=dict(user=dict(username=user.username)))))
But it is not happening. What could be the issue?
Here's my idea:
have a Django Website that receives / send JSON information so that I can create a JavaScript client for a Webbrowser, or a Unity / UE client
I want a new functionality only for Unity / UE client: realtime chat.
I'd like to use a tornado server on a specific port, let's say 8666.
Here's what I've done so far:
authenticate on the Django web site
make everything work on the Django web site
Now I'd like the client to connect to the port 8666 (pure TCP) and to send something (maybe his session cookie or something else) so that I can see on the tornado web server whether the client is authenticated, and look in the database to find out which other mates are connected too on the tornado webserver, so that when this client writes something, I can dispatch his message to all other "concerned" connected clients.
I didn't find any documentation about that. Do you know how to handle this? Any example, or if I'm not on the right track what should I do then?
If your Tornado process runs on the same domain as your Django application, the session cookie will be sent by the browser upon websocket handshake, and accessible through the WebSocketHandler.get_cookie() method.
Here is an example, assuming a global variable CLIENTS keeping track of connected authentified clients:
def open(self):
"""Authenticate client based on session cookie, add broadcast notification"""
session_id = self.get_cookie('sessionid')
if not session_id:
self.close()
self.authenticate_user(session_id)
if self.user is None:
self.close()
self.CLIENTS.append(self)
self.notify_all_clients()
def authenticate_user(self, session_id):
"""Retrieve User instance associated to the session key."""
session = SessionStore(session_key=session_id)
user_id = session.get('_auth_user_id')
if user_id is None:
return
try:
user = User.objects.get(pk=user_id)
except User.DoesNotExist:
self.close()
else:
self.user = user
Hope this helps!
Edit:
Note that to be able to use the Django ORM, you must set the DJANGO_SETTINGS_MODULE environment variable to your app's settings module path (e.g. 'myapp.settings', making sure it can be found through sys.path) and then call setup() as explained in the Django docs.
I'm using Django with MongoDB as a back-end database.
In settings.py I already set these codes:
SESSION_ENGINE = 'mongoengine.django.sessions'
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.sessions',
And in my view/user.py (only part of the code)
if user.password == password:
#add session information
if request.session.test_cookie_worked():
returnmsg = "COOKIE OK"
else:
returnmsg = "COOKIE ERR"
response = HttpResponse(returnmsg)
response.set_cookie("username", username)
request.session['username'] = username
user.log.append(UserLog(time=datetime.now(), ip=request.META['REMOTE_ADDR'], login=True))
user.save()
return response
When I run my site, I could login by this function and other functions which need verify my session setting could read username from response.session.
But the returnmsg is COOKIE ERR, and when I use Web Inspector of Safari, I can see no session or cookie here.
Where does Django store it session data? Should it be an encoded cookie on client's computer? If it does not store on my client's computer, why Django still could get that?
And in my code I also tried just set a cookie directly, it still does not work.
Is there something wrong about MongoEngine? Did I use it in the wrong way?
You should set a test cookie first with set_test_cookie(), and then on a subsequent request check for it with test_cookie_worked().
The function doesn't test if cookies work, it checks specifically if the test cookie was successfully set - which has a consequence of checking if cookies work.
Unless you use set_test_cookie(), test_cookie_worked() will always fail.
I would like to audit when a user has experienced an idle timeout in my Django application. In other words, if the user's session cookie's expiration date exceeds the SESSION_COOKIE_AGE found in settings.py, the user is redirected to the login page. When that occurs, an audit should also occur. By "audit", I mean a record should be written to my person.audit table.
Currently, I have configured some middleware to capture these events. Unfortunately, Django generates a new cookie when the user is redirected to the login page, so I cannot determine if the user was taken to the login page via an idle timeout or some other event.
From what I can tell, I would need to work with the "django_session" table. However, the records in this table cannot be associated with that user because the sessionid value in the cookie is reset when the redirect occurs.
I'm guessing I'm not the first to encounter this dilemma. Does anyone have insight into how to resolve the problem?
Update:
After a bit of testing, I realize that the code below doesn't answer your question. Although it works, and the signal handler gets called, prev_session_data if it exists, won't contain any useful information.
First, an inside peek at the sessions framework:
When a new visitor requests an application URL, a new session is generated for them - at this point, they're still anonymous (request.user is an instance of AnonymousUser).
If they request a view that requires authentication, they're redirected to the login view.
When the login view is requested, it sets a test value in the user's session (SessionStore._session); this automatically sets the accessed and modified flags on the current session.
During the response phase of the above request, the SessionMiddleware saves the current session, effectively creating a new Session instance in the django_session table (if you're using the default database-backed sessions, provided by django.contrib.sessions.backends.db). The id of the new session is saved in the settings.SESSION_COOKIE_NAME cookie.
When the user types in their username and password and submits the form, they are authenticated. If authentication succeeds, the login method from django.contrib.auth is called. login checks if the current session contains a user ID; if it does, and the ID is the same as the ID of the logged in user, SessionStore.cycle_key is called to create a new session key, while retaining the session data. Otherwise, SessionStore.flush is called, to remove all data and generate a new session. Both these methods should delete the previous session (for the anonymous user), and call SessionStore.create to create a new session.
At this point, the user is authenticated, and they have a new session. Their ID is saved in the session, along with the backend used to authenticate them. The session middleware saves this data to the database, and saves their new session ID in settings.SESSION_COOKIE_NAME.
So you see, the big problem with the previous solution is by the time create gets called (step 5.), the previous session's ID is long gone. As others have pointed out, this happens because once the session cookie expires, it is silently deleted by the browser.
Building on Alex Gaynor's suggestion, I think I've come up with another approach, that seems to do what you're asking, though it's still a little rough around the edges. Basically, I use a second long-lived "audit" cookie, to mirror the session ID, and some middleware to check for the presence of that cookie. For any request:
if neither the audit cookie nor the session cookie exist, this is probably a new user
if the audit cookie exists, but the session cookie doesn't, this is probably a user whose session just expired
if both cookies exist, and have the same value, this is an active session
Here's the code so far:
sessionaudit.middleware.py:
from django.conf import settings
from django.db.models import signals
from django.utils.http import cookie_date
import time
session_expired = signals.Signal(providing_args=['previous_session_key'])
AUDIT_COOKIE_NAME = 'sessionaudit'
class SessionAuditMiddleware(object):
def process_request(self, request):
# The 'print' statements are helpful if you're using the development server
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
audit_cookie = request.COOKIES.get(AUDIT_COOKIE_NAME, None)
if audit_cookie is None and session_key is None:
print "** Got new user **"
elif audit_cookie and session_key is None:
print "** User session expired, Session ID: %s **" % audit_cookie
session_expired.send(self.__class__, previous_session_key=audit_cookie)
elif audit_cookie == session_key:
print "** User session active, Session ID: %s **" % audit_cookie
def process_response(self, request, response):
if request.session.session_key:
audit_cookie = request.COOKIES.get(AUDIT_COOKIE_NAME, None)
if audit_cookie != request.session.session_key:
# New Session ID - update audit cookie:
max_age = 60 * 60 * 24 * 365 # 1 year
expires_time = time.time() + max_age
expires = cookie_date(expires_time)
response.set_cookie(
AUDIT_COOKIE_NAME,
request.session.session_key,
max_age=max_age,
expires=expires,
domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH,
secure=settings.SESSION_COOKIE_SECURE or None
)
return response
audit.models.py:
from django.contrib.sessions.models import Session
from sessionaudit.middleware import session_expired
def audit_session_expire(sender, **kwargs):
try:
prev_session = Session.objects.get(session_key=kwargs['previous_session_key'])
prev_session_data = prev_session.get_decoded()
user_id = prev_session_data.get('_auth_user_id')
except Session.DoesNotExist:
pass
session_expired.connect(audit_session_expire)
settings.py:
MIDDLEWARE_CLASSES = (
...
'django.contrib.sessions.middleware.SessionMiddleware',
'sessionaudit.middleware.SessionAuditMiddleware',
...
)
INSTALLED_APPS = (
...
'django.contrib.sessions',
'audit',
...
)
If you're using this, you should implement a custom logout view, that explicitly deletes the audit cookie when the user logs out. Also, I'd suggest using the django signed-cookies middleware (but you're probably already doing that, aren't you?)
OLD:
I think you should be able to do this using a custom session backend. Here's some (untested) sample code:
from django.contrib.sessions.backends.db import SessionStore as DBStore
from django.db.models import signals
session_created = signals.Signal(providing_args=['previous_session_key', 'new_session_key'])
class SessionStore(DBStore):
"""
Override the default database session store.
The `create` method is called by the framework to:
* Create a new session, if we have a new user
* Generate a new session, if the current user's session has expired
What we want to do is override this method, so we can send a signal
whenever it is called.
"""
def create(self):
# Save the current session ID:
prev_session_id = self.session_key
# Call the superclass 'create' to create a new session:
super(SessionStore, self).create()
# We should have a new session - raise 'session_created' signal:
session_created.send(self.__class__, previous_session_key=prev_session_id, new_session_key=self.session_key)
Save the code above as 'customdb.py' and add that to your django project. In your settings.py, set or replace 'SESSION_ENGINE' with the path to the above file, e.g.:
SESSION_ENGINE = 'yourproject.customdb'
Then in your middleware, or models.py, provide a handler for the 'session_created' signal, like so:
from django.contrib.sessions.models import Session
from yourproject.customdb import session_created
def audit_session_expire(sender, **kwargs):
# remember that 'previous_session_key' can be None if we have a new user
try:
prev_session = Session.objects.get(kwargs['previous_session_key'])
prev_session_data = prev_session.get_decoded()
user_id = prev_session_data['_auth_user_id']
# do something with the user_id
except Session.DoesNotExist:
# new user; do something else...
session_created.connect(audit_session_expire)
Don't forget to include the app containing the models.py in INSTALLED_APPS.
I don't know about Django, but can you, simply create a non-persistent cookie, which stores the last access time to a page on your site (you update the cookie on each page load)
Then, on your login page, you can check if your user has your cookie, but no session, then, you know that the user's session has probably timed out. Since you have the time of the last access to a page on your site, you can also calculate, based on the duration of the session, if it has timed out.
SESSION_COOKIE_AGE = 1500 # 25 minutes
Put that in your settings and that should take care of that and expire the session.