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.
Related
I am working on a Django+React project. My React app is my only UI renderer (which means no template or any markup is being served by Django), while my Django app is only used for serving APIs for me to access my database resources.
On a particular React page, I have login and test buttons that send requests to a DRF API in my Django backend.
Login API View
class AuthLogin(generics.GenericAPIView):
serializer_class = LoginSerializer
authentication_classes = [SessionAuthentication]
permission_classes = ()
def get(self, request):
user = request.user
if not user and not user.is_authenticated or user.is_anonymous:
return Response(
{
"status": True,
"message": "Login page accessed"
}, status=status.HTTP_200_OK
)
else:
return Response(
{
"status": False,
"message": "You are currently logged in"
}, status=status.HTTP_400_BAD_REQUEST
)
def post(self, request, *args, **kwargs):
res = []
res_stat = status.HTTP_401_UNAUTHORIZED
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=False)
validated_data = serializer.validated_data
if validated_data:
user = dj_auth(username=request.data.get('username'),
password=request.data.get('password'))
if user and user.is_authenticated and not user.is_anonymous:
auth_login(request, user)
res_stat = status.HTTP_200_OK
res = {
"status": True,
"message": "Login successful!",
"login": {
"date_time": timezone.now(),
"user": {
'id': user.id,
'first_name': user.first_name,
'last_name': user.last_name,
'email': user.email,
}
}
}
else:
res_stat = status.HTTP_401_UNAUTHORIZED
res = {
"status": False,
"message": "Invalid username and/or password"
}
else:
res_stat = status.HTTP_401_UNAUTHORIZED
res = {
"status": False,
"message": "Invalid username and/or password"
}
return Response(res, status=res_stat)
Test API View
class Test(APIView):
authentication_classes = [SessionAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request, *args, **kwargs):
if request.user and request.user.is_authenticated and not request.user.is_anonymous:
return Response({
"test": "coolsss",
"user": request.user.id
}, status=status.HTTP_200_OK)
else:
return Response({
"error": "Mind to login first?"
}, status=status.HTTP_401_UNAUTHORIZED)
So far so good in terms of Django+React integration on the above APIs. However, I noticed a security issue and I'm not sure completely how to move on from this.
I did the test in an incognito browser window. But, I also tested it in Postman. What I noticed was I already closed my incognito browser before testing on to Postman using the same csrftoken and sessionid cookies. When I closed the browser, I assume the session is destroyed since it was in incognito.
Now when I run the same in Postman, the test endpoint still authenticating, recognizes the user, and returns the positive response. I'm not sure if that is supposed to happen, and what approach shall be used to resolve this.
One sample scenario I see where this would be a nightmare is say Kate logs in and leaves her workstation for a minute. Her workmate John sneaks in to her browser and steals the csrftoken and sessionid cookie values of her current session.
Kate came back and for some reason closed her browser. John, using Postman on his own computer, made some POST request to an API which Kate is authorized to access using the token and sessionid he stole.
With this problem in session/cookie-based authentication, I am considering implementing token-based authentication instead, even if the login view is more appropriate to be authenticated by a server session and csrftoken cookie.
However, if I use token-based, I would also be storing that token in local storage so React will have the knowledge that the user is still logged in and for it to use that token for subsequent requests. But then again, a geek can always go to dev tool and sneak into that token value in the local storage.
I am so stuck at how I should properly implement authentication and security in the backend, while keeping the frontend of the user's login state.
Token-based (JWT for example) is the go-to option for integrating a server-side service to a client-side app. And as for the problem you are facing, the simplest option would be adding an expire option to the token which still might not be the solution to all scenarios.
The fact is that when you create a client-side app, the client has control and you can't do much about it. protecting the token is the same as protecting a password that users should be thinking about. The most you can do is to make it harder for someone else to be able to simply take the token and use it somewhere else.
You can make multiple tokens with different specifications that the client-side has to generate and if all of them match, then you can serve the request. but again, that just makes it harder for someone else to duplicate the request not impossible.
If it was a compiled app that was hard to decompile and with the combination of some methods like using some certificate file that is also securely stored in the client's system, you could be more sure about the safety of your request but JS is too simple to read and duplicate.
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?
TL;DR: Is it OK to not store (Google/Facebook) OAuth2 access tokens, but rather request new ones on every login?
I'll explain:
To add social logins ("Login with Google/Facebook") to a Django app, you have Python Social Auth. The problem with it, at least for me, is that it's much more complicated than I'd like, requires a lot of configurations, creates additional tables in the database, and in general feels like a lot of moving parts I don't understand for a rather simple task.
So, I read a bit about Google's and Facebook's flows, and they're simple enough:
The server has an ID and a secret.
The clients/users have their standard login credentials.
Then:
The server redirects the user to Google/Facebook, and provides its ID and a redirection URI.
After the user has logged in, Google/Facebook redirects them to that URI with a code.
The server sends its ID, secret, and the received code to Google/Facebook, and gets an access token in exchange, which it can now use to make API calls on behalf of the user.
Even the most basic permissions are enough to query Google/Facebook for the user's email, which can then be matched against Django's standard User model (and if it doesn't exist, a password-less user can created).
The access token can now be discarded, as it was only necessary to translate the Google/Facebook login to an actual email (validated by them), which is used as the only user identifier - no more confusion due to different accounts when logging via different social services; no additional tables; no unnecessary complexity.
Here's the code, just to show how little is necessary:
# views.py
def login_view(request):
return render(request, 'login.html', {
'google_url': 'https://accounts.google.com/o/oauth2/v2/auth?' + urllib.parse.urlencode({
'client_id': GOOGLE_ID,
'redirect_uri': request.build_absolute_uri(reverse('oauth_google')),
'scope': 'profile email',
'response_type': 'code',
}),
}) # use this url in the template as a social login
def oauth_google(request):
code = request.GET['code']
response = requests.post('https://www.googleapis.com/oauth2/v4/token', {
'code': code,
'client_id': GOOGLE_ID,
'client_secret': GOOGLE_SECRET,
'redirect_uri': request.build_absolute_uri(reverse('oauth_google')),
'grant_type': 'authorization_code',
}).json()
id_token = response['id_token']
response = requests.get('https://www.googleapis.com/oauth2/v3/tokeninfo', {
'id_token': id_token,
}).json()
email = response['email']
user = User.objects.filter(username=email).first()
if not user:
user = User(username=email)
user.save()
auth.login(request, user)
return redirect('index')
# urls.py
urlpatterns = [
...
path('oauth/google', views.oauth_google, name='oauth_google'),
]
My question is: what am I missing? If it's really that simple, why couldn't I find any answers on StackOverflow / tutorials on the web describing just that?
The only reason I can think of is that access tokens come with an expiration time (anywhere between 1 hour and 60 days); so maybe I'm supposed to keep reusing the same access token as long as it's valid (which will require storing it, and would explain why Python Social Auth needs additional tables). Will Google/Facebook get mad at me for requesting new access tokens too frequently and block me? I couldn't find any mention of this in their documentation.
EDIT
Here's the Facebook code, in case anyone finds these snippets useful:
# views.py
def login_view(request):
return render(request, 'login.html', {
'facebook_url': 'https://www.facebook.com/v3.2/dialog/oauth?' + urllib.parse.urlencode({
'client_id': FACEBOOK_ID,
'redirect_uri': request.build_absolute_uri(reverse('oauth_facebook')),
'scope': 'email',
}),
}) # use this url in the template as a social login
def oauth_facebook(request):
code = request.GET['code']
response = requests.get('https://graph.facebook.com/v3.2/oauth/access_token', {
'code': code,
'client_id': FACEBOOK_ID,
'client_secret': FACEBOOK_SECRET,
'redirect_uri': request.build_absolute_uri(reverse('oauth_facebook')),
}).json()
access_token = response['access_token']
response = requests.get('https://graph.facebook.com/me', {
'access_token': access_token,
'fields': 'email',
}).json()
email = response['email']
user = User.objects.filter(username=email).first()
if not user:
user = User(username=email)
user.save()
auth.login(request, user)
return redirect('index')
# urls.py
urlpatterns = [
...
path('oauth/facebook', views.oauth_facebook, name='oauth_facebook'),
]
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?
I use Angular 2 as frontend, django rest framework as backend.
At frontend I use Auth0 for authenticating user (https://auth0.com/docs/quickstart/spa/angular2 ). and after I send the idtoken to my backend for creating news users (https://auth0.com/docs/quickstart/backend/python connected auth0
the code in angular 2:
import { Component } from '#angular/core';
import { Auth } from './auth.service';
import { AuthHttp } from 'angular2-jwt';
import { Http } from '#angular/http';
import 'rxjs/add/operator/map';
#Component({
selector: 'ping',
templateUrl: 'app/ping.template.html'
})
export class PingComponent {
API_URL: string = 'http://localhost:8000/callback/';
message: string;
constructor(private auth: Auth, private http: Http, private authHttp: AuthHttp) {}
// the code for sending idtoken to my backend
//correct me please if I am wrong
public securedPing() {
this.message = '';
this.authHttp.post(`${this.API_URL}`,localStorage.getItem('id_token'))
.map(res => res.json())
.subscribe(
data => this.message= data.text,
error => this.message = error._body || error
);
}
};
and here the code of my backend in Django :
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.http import JsonResponse
from places_management.serializers import UserSerializer
from django.contrib.auth.models import User
import jwt
class Callbacks(APIView):
authentication_classes = []
permission_classes = []
def authenticate(error):
return Response(error,401)
def post(self, request, format=None):
"""
Callback for after user logs in. It creates a
django auth user if one does not exist, username is the
user_id retured frrom auth0
"""
#token = request.META['HTTP_AUTHORIZATION'].split('JWT ')[1]
auth = request.META.get('HTTP_AUTHORIZATION', None)
if not auth:
return Response({'code': 'authorization_header_missing', 'description': 'Authorization header is expected'},status=status.HTTP_401_UNAUTHORIZED)
parts = auth.split()
if parts[0].lower() != 'bearer':
return authenticate({'code': 'invalid_header', 'description': 'Authorization header must start with Bearer'})
elif len(parts) == 1:
return authenticate({'code': 'invalid_header', 'description': 'Token not found'})
elif len(parts) > 2:
return authenticate({'code': 'invalid_header', 'description': 'Authorization header must be Bearer + \s + token'})
token = parts[1]
try:
payload = jwt.decode(
token,
'Z-HWF9cDxGTk7aMZe0A2Ygt81vGBPihz1FCRzJfS87B0mCw1ClQzp1HgA7U3WsSg',
audience='GwtnxdwhMWsuGz6JxabDkrNvFhAvn5ZJS'
)
except jwt.ExpiredSignature:
return authenticate({'code': 'token_expired', 'description': 'token is expired'})
except jwt.InvalidAudienceError:
return authenticate({'code': 'invalid_audience', 'description': 'incorrect audience, expected: GwtnxdwhMWsuGz6JxabDkrNvFhvn5ZJS'})
except jwt.DecodeError:
return authenticate({'code': 'token_invalid_signature', 'description': 'token signature is invalid'})
#try:
#payload = settings.JWT_AUTH['JWT_DECODE_HANDLER'](token)
#except:
#return Response('text',status=status.HTTP_401_UNAUTHORIZED)
user = User.objects.filter(username=payload['sub']).first()
if(user is None):
password = User.objects.make_random_password()
user = User.objects.create_user(
username=payload['sub'], password=password)
serializer = UserSerializer(user, context={'user': user})
return Response(serializer.data, status=status.HTTP_200_OK)
Maybe I am wrong, please help
As usual in software development there's more than one way to approach a given problem and the most suitable solution usually requires delving into a lot of small details.
However, there are a few things that are safe to say based on the information you shared. If you're leveraging Auth0 for use authentication you should not then be creating users in your back-end with passwords. Passwords are for authentication and you delegated that part, which is a good thing, as it's less work for you.
Having said that it's completely normal to still have some kind of per-user storage associated with your application, for example, to store user-specific application settings or other user owned data.
For this you'll have to store that data in association with a user identifier that can be safely used to identify the same user across multiple authentication sessions. That identifier should be the sub claim value contained within the ID Token given that per specification that value is stable and will allow you to identify recurring users when they complete the authentication through Auth0.
In conclusion, you back-end should not create new user identities as that is within the scope of authentication which is handled by Auth0; instead you should store data in association with a user identifier that allows you to continuously identify recurring users.
Please Refer , It do contain full demo of Rest Api With JWT, Auth() , Api Integrated in front end with angular(1) js
https://github.com/rmemon/Angular-JS-django-rest-framework-jwt