Django Channels : Login to the session using consumer not working - django

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?

Related

Django session is not being persisted and cookie is not being set when using Django Channels

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).

Django Channels session is not persistent. disconnect -> logged out

I have a websocket-router:
application = ProtocolTypeRouter({
'websocket':
AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(
[
url("ws/", Consumer)
]
)
)
)
})
I am logging in a user by sending a command to the websocket. The user gets logged in like this:
if cmd == 'login':
user = await database_sync_to_async(authenticate)(consumer.scope, email=request['eMail'], password=request['pass'])
if user is not None:
# login the user to this session.
await login(consumer.scope, user, backend='allauth.account.auth_backends.AuthenticationBackend')
# save the session
consumer.scope['session'].modified = True
await database_sync_to_async(consumer.scope['session'].save)()
Every time, the websocket-connection gets disconnected, the user is not logged in anymore.
I thought, the session gets saved by
consumer.scope['session'].save()
but it doesn´t work. The session is not persistent.
How can I solve this problem?
which session backend are you using in Django.
Due to how web sockets work once the connection is created you can't set any cookies so if your using a session backend that depends on cookie storage the save will have no effect since the web-browser can't be updated.
Currently channels does not even support setting cookies durring the accept method. https://github.com/django/channels/issues/1096#issuecomment-619590028
However if you ensure that you users already has a session cookie then you can upgrade that session to a logged in user.

How to use Django's session authentication with Django Channels?

I am developing a chess web app using Django, Django Channels and React. I am using websockets for the game play between the online players and for getting updated which players are now online and available to play. However I am stuck on the authentication part. I first started with token authentication, but I found that it is not possible to send custom headers with the token in them as part of the websocket request. Then I went back to the default django.contrib.auth session authentication. Unfortunately, when the client logs in and connects to the websocket I am not able to get their user info as if the user is using a different session with the websocket. I get the value AnonymousUser when I print self.scope["user"] in the websocket consumers. Note that I am able to exchange messages using the websocket, and the authentication works well with normal http requests as I can prevent users who are not logged in from accessing views.
I am guessing the problem is related to the fact that websocket requests on the client side don't access or use the cookie for the authentication like the http requests.
Has anybody faced a similar problem and how did they fix it?
This is how I send the websocket message in react:
submitMessage = (evt) => {
//console.log("token in Messenger=",this.props.token);
evt.preventDefault();
const message = { message: this.state.message_to_send}
this.ws.send(JSON.stringify(message))
}
This is the backend code for handling the websocket requests:
from channels.generic.websocket import WebsocketConsumer
import json
from asgiref.sync import async_to_sync
class LoggedUsersConsumer(WebsocketConsumer):
def connect(self):
self.user = self.scope["user"]
print(self.scope)
print(self.user,"+++++++++++++")
#Join group
async_to_sync(self.channel_layer.group_add)(
"logged_users",
self.channel_name
)
self.accept()
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
"logged_users",
self.channel_name
)
def receive(self, text_data):
self.user = self.scope["user"]
print(self.user,"+++++++++++++")
text_data_json = json.loads(text_data)
print(text_data_json)
message = text_data_json['message']
# Send message to room group
async_to_sync(self.channel_layer.group_send)(
"logged_users",
{
'type': 'logged_user_message',
'message': message
}
)
def logged_user_message(self, event):
message = event['message']
# Send message to WebSocket
self.send(text_data=json.dumps({
'message': message
}))
I think you are right, you probably do not have a session-cookie for requests from the client side that's why you get AnonymousUser. I don't think this has anything to do with how you handle websocket requests neither in React nor in Django.
Please check your browser's cookies in your React frontend (through the Developer Tools in Chrome/Firefox). You should have at least 2 cookies, csrftoken and sessionid. If any of these are lacking, the following might help you in the right direction. I experienced the same when developing with Vue, Django Channels and Django Rest Framework.
If you visit your Django backend through your browser, the HTML-templates and your browser takes care of setting the cookies. When you do this from React or Vue, the HTML is not rendered. Therefore you need to implement authentication and the setting of cookies yourself. And of course you need to authenticate from React in the same session as you later use for accessing the web-sockets.
I use the following Django-views to authenticate from the frontend:
#api_view()
#permission_classes([AllowAny])
#ensure_csrf_cookie
#csrf_exempt
def session_info(request):
"""
View to retrieve user info for current user. (Can be adapted to your needs). If user is not logged in, view will
still return CSRF cookie which in neccessary for authentication.
"""
if not request.user.is_authenticated:
return Response({"message": "Not authenticated.", "authenticated": False})
return Response(
{"message": "Authenticated.", "authenticated": True, "user": str(request.user)}
)
#api_view(['POST'])
#permission_classes([AllowAny])
def session_auth(request):
"""
Login-view.
"""
username = request.data['username']
password = request.data['password']
user = authenticate(request, username=username, password=password)
if user is not None:
if user.is_active:
login(request, user)
request.session['authenticated_user'] = user.username
return Response(
{
"message": "Authenticated.",
"authenticated": True,
"name": user.name,
}
)
return Response({"message": "Not authenticated", "authenticated": False})
In your urls.py you need to add something like the following:
urlpatterns = [
path(
'session/',
views.session_info,
name='session',
),
path(
'sessionauth/',
views.session_auth,
name='sessionauth',
),
]
Now you can from your frontend do something like this (the following is my javascript/Vue-code, slightly adapted for this post, but you can probably do something similar with React):
// Please note: this code can probably be improved a lot as my skills in javascript are not the best
login: ({ username, password }) => {
window.$cookies.set("username", username);
const session_url = `${BACKEND_URL}/api/v1/session/`;
const url = `${BACKEND_URL}/api/v1/sessionauth/`;
axios.get(session_url).then(response => {
axios.defaults.headers.common["Authorization"] = "";
// NOTE: the CSRF-cookie need to be included in subsequent POSTs:
axios.defaults.headers.post["X-CSRF-Token"] = response.data._csrf;
axios
.post(url, { username: username, password: password })
.then(response => {
axios.get(session_url);
})
.catch(e => {
console.log("ERROR: ", e);
commit("SET_LOGIN_ERROR", e);
});
});
I hope this is of some help. If not, let me know.
Let me just add this in case it might help someone,
You always get AnonymousUser because probably you are not passing the cookie header right.
with session authentcation, pass the session header like this.
Cookie: csrftoken=wBD7Qri09dyAR8oMY8fuL1nqCOvGGmaO; sessionid=hpb8xx873holf3zl01wfe6f7511bhxqi
You might try to do
Set-Cookie: sessionid=hpb8xx873holf3zl01wfe6f7511bhxqi;
Set-Cookie: csrftoken=wBD7Qri09dyAR8oMY8fuL1nqCOvGGmaO;
That won't work.

Google calendar authentication page opens in terminal instead of client browser

I am integrating google calendar with my web application which is a django app. when i am doing it on localhost server, its working fine. Google authentication page opens in client browser, but when i am uploading that code to the server and integrating google calendar, then Google authentication page opens in terminal where i run my django server.
This is the page that opens for authentication in terminal
I want to provide this auth through client web browser.
`
def get_credentials(request):
creds = None
# If there are no (valid) credentials available, let the user log in.
if os.path.exists('token.pickle_' + request.GET.get('bot_id')):
with open('token.pickle_' + request.GET.get('bot_id'), 'rb') as token:
creds = pickle.load(token)
print(creds)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
CLIENT_SECRET_FILE, SCOPES)
creds = flow.run_local_server()
# Save the credentials for the next run
with open('token.pickle_' + request.GET.get('bot_id'), 'wb') as token:
pickle.dump(creds, token)
serializer = CalenderIntegrationSerializer(data={'bot_id': int(request.GET.get('bot_id')), 'status': True})
if serializer.is_valid():
serializer.save()
if os.path.exists('token.pickle_' + request.GET.get('bot_id')):
context = {'signin_url': creds}
return JsonResponse({'status': 200, 'data': 'Integration done!', 'is_integrated': True})
`
And this is my reference google calendar code python
This code is specifically for local development:
https://developers.google.com/api-client-library/python/auth/installed-app
The code gives you a hint on how to construct the URL, which you then need to send back to your user as a temporary redirect; e.g. using the redirect function. Then you need to have a django handler which accepts the redirect and executes the second half of the function. So:
Split your code into two functions.
Build a url, send a redirect with your endpoint as the callback
Google will redirect back to your endpoint after the user completes the flow.
Parse the results
Execute your code.

How to store flask sessions on serverless APP

I'm trying to build a serverless Flask APP. To login users, I use auth0.com.
After the user logs in I get an access token, I send a post request with it to my flask backend and there I exchange the token for the user info doing this:
#app.route('/callback', methods=['POST'])
#cross_origin()
def callback_handling():
resp = request.get_json()
url = 'https://' + AUTH0_DOMAIN + '/userinfo'
headers = {'authorization': 'Bearer ' + resp['access_token']}
r = requests.get(url, headers=headers)
userinfo = r.json()
# Store the tue user information in flask session.
session['jwt_payload'] = userinfo
session['profile'] = {
'user_id': userinfo['sub'],
'name': userinfo['name'],
'picture': userinfo['picture']
}
Once I've done this I redirect the user to their dashboard. There I send a second post request to fetch the user profile, something like this:
#app.route('/profile', methods=['POST'])
#cross_origin()
def user_profile():
if 'profile' in session:
return jsonify({'profile':session['profile']})
else:
return jsonify({'profile':"Not logged in"})
This second function returns always {'profile':"Not logged in"}.
So I'm wondering what's the best way to do this. Should I always send back the auth0 token, send a request to them to ask who is the user and then return his data? It seems like an overkill to send always a request to auth0 everytime I need to return some data. Is there a better method?