How to store flask sessions on serverless APP - flask

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?

Related

Django sessionid not created for Locust client

I'm running Django 2.2.24 and Locust 2.1.0 for load testing. When I run some Locust test logging in as a the Django admin user, there are no issues - all my GETs work as expected.
Now I am actually logging in as a specific user. From a web interface, Django passes a CSRFtoken and a sessionid token with no problem. From the Locust client however, the sessionid does not show up at all. Not only that, but when I look in the django_session table (where the web tokens do exist), there are no tokens for the Locust client.
I think this is more related to Django session management than to locust - hence the post in this forum.
My locust file looks like this:
def on_start(self):
'''
The start script for each user - this order is important
'''
# Below 3 lines work fine - we get the csrftoken and put it in the header successfully
response = self.client.get("/accounts/login")
self.csrftoken = response.cookies['csrftoken']
self.headers = {'X-CSRFToken': self.csrftoken}
# Now login with username and password as POST
r1 = self.login()
return r1
def login(self):
# admin login and retrieving it's access token
udata = {'username': self.username, 'password': self.password}
cookies=self.client.cookies.get_dict())
#csrftoken cookie does exist, sessionid does not yet.
log.info("Current cookies in Login:" + str(self.client.cookies))
# This next line should come back with a sessionid
# from Django - but it does not.
response = self.client.post("/accounts/login/",
data=json.dumps(udata),
headers=self.headers)
log.info("Response from client.post="+str(response)) #OK
log.info("Response status code:" + str(response.status_code))
log.info("Response text=" + response.text)
# Next line does not contain sessionid or Set-Cookie
log.info("Headers from post/accts/login = " + str(response.headers))
Thanks for any assistance.
Change
response = self.client.post("/accounts/login/",
data=json.dumps(udata),
headers=self.headers)
to
response = self.client.post("/accounts/login/",
json=udata,
headers=self.headers)
… to get the appropriate json headers. Or use:
response = self.client.post("/accounts/login/",
data=udata,
headers=self.headers)
in order to send it as form-encoded data instead of json.

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.

Ionic Google social authentication to Django Rest Framework backend

I am trying to get social authentication working for my mobile app (an Ionic app on Android). Django rest framework backend with rest_framework_jwt, social_django, and rest_social_auth.
On my Ionic app I was using satellizer.js, however, I can't use InAppBrowser so now I am trying to do the following with cordova-plugin-googleplus:
Step#1 (On client/app)
if (provider == 'google') {
// Use Google API Natively to get tokens and user info
window.plugins.googleplus.login(
{
// TODO Get the WebClient from App settings
'webClientId': '[*.myclientid]', // optional clientId of your Web application from Credentials settings of your project - On Android, this MUST be included to get an idToken. On iOS, it is not required.
'offline': true, // optional, but requires the webClientId - if set to true the plugin will also return a serverAuthCode, which can be used to grant offline access to a non-Google server
}) ................
Result: This gets me a valid response with both a idToken, serverAuthCode, and a userId.
Step#2
I am not sure what the next step is. Originally, I was going to try using Django rest_social_auth to do the following from my client/app:
POST /api/login/social/
with data (json)
provider=google&code=ASLKDJASLDKJASLD
Which was supposed to return a JWT token (from my understanding of the docs), however, it is not passing the JWTAuthMixin as there is no value returned from a call to get_authorization_header(request).split() in that Mixin. These means that nothing is returned to my client/app except a 400 error.
Am I supposed to be adding a header to my Ionic app POST when passing my idToken or serverAuthCode? Or am I on the wrong side of the tracks...
Are there any implementation recommendations for this auth flow?
So far I did the following and it works.
1. On app/client
(The client uses satellizer.js and the cordova-plugin-googleplus)
if (provider == 'google') {
// Use Google API Natively to get tokens and user info
window.plugins.googleplus.login(
{
// TODO Get the WebClient from App settings
'webClientId': '*[googleclientid]*.apps.googleusercontent.com',
'offline': true
},
function (obj) {
$http.post(SERVER.url + '[MY BACKEND URL]' + '/google-oauth2/', {code: obj.idToken, servAuthCode: obj.serverAuthCode})
.success(function(data){
$auth.setToken(data.jwt_token);
/.. Do something ../
})
.error(function(data){
console.log("There was an error" + JSON.stringify(data));
});
},
function (msg) {
// TODO Set Error states
console.error('error: ' + msg);
}
);
}
Summary
The app calls the Google plus API googleplus.login method (sending my webClientId)
I post the resulting idToken and serverAuthCode obtained from google after login to my Django backend.
2. My backend methods
URL
My app/client hits the url(r'^[MY BACKEND URL]/(?P<backend>[\w-]+)/$', ObtainAuthToken.as_view(), ),
View
This calls the following view and functions:
class ObtainAuthToken(APIView):
permission_classes = (AllowAny,)
def post(self, request, backend):
data = request.data
user_tokenID = data['code']
server_auth_code = data['servAuthCode']
if user_tokenID and server_auth_code and verify_google_user_token_ID(user_tokenID):
# Get Google OAuth credentials for the verified GOOGLE user.
credentials = settings.GOOGLE_FLOW.step2_exchange(server_auth_code)
# Here we call PSA to authenticate like we would if we used PSA on server side.
user = register_by_access_token(request, backend, token=credentials.access_token)
# If user is active we get or create the REST token and send it back with user data
if user and user.is_active:
# Generate JWT token for user and pass back to client
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return JsonResponse({'id': user.id, 'name': user.username, 'jwt_token': token})
return JsonResponse({'status':'false','error':'Bad Credentials, check the Access Token and/or the UID'},
status=403)
def verify_google_user_token_ID(user_tokenID):
try:
google_http_request = google.auth.transport.requests.Request()
idinfo = verify_token(user_tokenID, request=google_http_request,
audience=settings.SOCIAL_AUTH_GOOGLE_OAUTH2_FULL_KEY)
# Or, if multiple clients access the backend server:
if idinfo['aud'] not in [settings.GOOGLE_APP_ID_ANDROID, settings.GOOGLE_APP_ID_WEB]:
raise crypt.AppIdentityError("Unrecognized client.")
if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
raise crypt.AppIdentityError("Wrong issuer.")
return True
except crypt.AppIdentityError as e:
# Invalid token
return False
#psa('social:complete')
def register_by_access_token(request, backend, token):
backend = social_core.backends.google.GoogleOAuth2()
user = backend.do_auth(access_token=token, backend=backend)
if user:
return user
else:
return None
3. Back on the client
My client then looks at the response and takes the returned JWT and loads it to memory with $auth.setToken(data.jwt_token);
I think this works for now, but I still have to deal with token refresh and revocation etc.

Facebook sign up using django allauth tastypie

I am working on a mobile app that allows users to sign up via Facebook. Once I receive the access token from FB, I send it to the Django backend.
I am using tastypie and django-allauth.
How can I use django-allauth to create the new user/social account using the access token?
I am using this code to login with already registered FB accounts, but when I try to signup with it, an error is raised that says:
'AnonymousUser' object has no attribute 'email'
def facebook_login(self, request, **kwargs):
self.method_check(request, allowed=['post'])
data = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'applicaton/json'))
if "access_token" not in request.body:
data = {"message": 'missing access token'}
return self.error_response(request, data, response_class=http.HttpBadRequest)
access_token = data.get('access_token', '')
try:
app = SocialApp.objects.get(provider="facebook")
token = SocialToken(app=app, token=access_token)
login = fb_complete_login(app, token)
login.token = token
login.state = SocialLogin.state_from_request(request)
ret = complete_social_login(request, login)
#if we get here we've succeeded
return self.create_response(request, {
'email': request.user.email,
'api_key': request.user.api_key.key
}, HttpAccepted)
except Exception as ex:
data = {"message": ex}
return self.error_response(request, data, response_class=http.HttpBadRequest)
This code actually works, The problem was setting SOCIALACCOUNT_AUTO_SIGNUP=false and that was causing the error.

python-social-auth not getting correct Google OAuth2 details

I want to login a user using the python-social-auth functionality for Google Plus signin in Django. When logging in from my website, everything works fine and the correct details are added to the database.
However, I want to authenticate from my Android application as well. The user logs in in the application, which then sends the access token to the django API, which handles the login process in the following code, adapted from the documentation:
#csrf_exempt
#serengeti_api_request
#psa('social:complete')
def login_social_token(request, backend):
# Ensure the token has been specified.
token = request.META.get('HTTP_ACCESSTOKEN')
if token is None:
raise SerengetiApiRequestException('Access token is missing!')
# Login the user for this session
user = request.backend.do_auth(token)
if user is None:
raise SerengetiApiRequestException('Could not authenticate user!')
login(request, user)
# Store the email address if one has been specified (e.g. Twitter)
email = request.META.get('HTTP_EMAIL')
if email is not None:
user.email = email
user.save()
# Prepare the parameters to be returned
response = dict({
'id': user.id,
'first_name': user.first_name,
'last_name': user.last_name,
'api_key': request.session.session_key,
})
# Return a 200 status code to signal success.
return HttpResponse(json.dumps(response, indent=4), status=200)
When logging in from the website, the social_auth_usersocialauth table contains:
id | provider | uid | extra_data
==========================================
10 | google-oauth2 | <myemail> | {"token_type": "Bearer", "access_token": "<token>", "expires": 3600}
However, when logging in from the application using the above function, the operation completes ok, but the entry in the table looks like this:
id | provider | uid | extra_data
=========================================
10 | google-oauth2 | <empty> | {"access_token": "", "expires": null}
Also, the auth_user table contains a username like eeed494412obfuscated48bc47dd9b instead of the Google Plus username and the email field is empty.
What am I doing wrong and how can I obtain the same functionality as I get on the website?
I would like to mention that I have implemented Facebook and Twitter authentication from the Android application, which call the above-mentioned function and store the correct details, only Google Plus is causing problems.
Just wanted to share an alternative way of doing this. This example is quite primitive and doesn't cover all cases (e.g. failed authentication). However, it should give enough insight into how OAuth2 authentication can be done.
Obtain CLIENT ID
Obtain a CLIENT ID from OAuth2 service provider (e.g. Google) and configure redirect URLs.
I assume you have already done this.
Create a login / registration link
You need to generate a login / registration link in your view. It should be something like this:
https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={{CLIENT_ID}}&redirect_uri={{REDIRECT_URL}}&scope=email
Replace {{CLIENT_ID}} and {{REDIRECT_URL}} with the details you obtained in the previous step.
Create a new view
In urls.py add something like:
url(r'^oauth2/google/$', views.oauth2_google),
In your views.py create a method:
def oauth2_google(request):
# Get the code after a successful signing
# Note: this does not cover the case when authentication fails
CODE = request.GET['code']
CLIENT_ID = 'xxxxx.apps.googleusercontent.com' # Edit this
CLIENT_SECRET = 'xxxxx' # Edit this
REDIRECT_URL = 'http://localhost:8000/oauth2/google' # Edit this
if CODE is not None:
payload = {
'grant_type': 'authorization_code',
'code': CODE,
'redirect_uri': REDIRECT_URL,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET
}
token_details_request = requests.post('https://accounts.google.com/o/oauth2/token', data=payload)
token_details = token_details_request.json()
id_token = token_details['id_token']
access_token = token_details['access_token']
# Retrieve the unique identifier for the social media account
decoded = jwt.decode(id_token, verify=False)
oauth_identifier = decoded['sub']
# Retrieve other account details
account_details_request = requests.get('https://www.googleapis.com/plus/v1/people/me?access_token=' + access_token)
account_details = account_details_request.json()
avatar = account_details['image']['url']
# Check if the user already has an account with us
try:
profile = Profile.objects.get(oauth_identifier=oauth_identifier)
profile.avatar = avatar
profile.save()
user = profile.user
except Profile.DoesNotExist:
user = User.objects.create_user()
user.save()
profile = Profile(user=user, oauth_identifier=oauth_identifier, avatar=avatar)
profile.save()
user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, user)
return redirect('/')
You might need the following imports:
from django.shortcuts import redirect
import jwt # PyJWT==0.4.1
import requests # requests==2.5.0
import json
I have a project (not running actually) with google oauth2 authentication. I leave here my config file so it may be useful to you (I was only using oauth2 so some things may vary):
AUTHENTICATION_BACKENDS = (
'social.backends.google.GoogleOAuth2', # /google-oauth2
'django.contrib.auth.backends.ModelBackend',
)
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'your google oauth 2 key'
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'your secret google oauth 2 key'
SOCIAL_AUTH_PIPELINE = (
'social.pipeline.social_auth.social_details',
'social.pipeline.social_auth.social_uid',
'social.pipeline.social_auth.auth_allowed',
'social.pipeline.social_auth.associate_by_email',
'social.pipeline.social_auth.social_user',
'social.pipeline.user.get_username',
'social.pipeline.user.create_user',
'social.pipeline.social_auth.associate_user',
'social.pipeline.social_auth.load_extra_data',
'social.pipeline.user.user_details'
)
I attach the view also (note that I'm using django rest framework).
class ObtainAuthToken(APIView):
permission_classes = (permissions.AllowAny,)
serializer_class = AuthTokenSerializer
model = Token
# Accept backend as a parameter and 'auth' for a login / pass
def post(self, request, backend):
if backend == 'auth': # For admin purposes
serializer = self.serializer_class(data=request.DATA)
if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user'])
return Response({'token': token.key})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
# Here we call PSA to authenticate like we would if we used PSA on server side.
user = register_by_access_token(request, backend)
# If user is active we get or create the REST token and send it back with user data
if user and user.is_active:
token, created = Token.objects.get_or_create(user=user)
return Response({'id': user.id, 'name': user.username, 'token': token.key})
else:
return Response("Bad Credentials, check the Access Token and/or the UID", status=403)
#strategy('social:complete')
def register_by_access_token(request, backend):
# This view expects an access_token GET parameter
token = request.GET.get('access_token')
backend = request.strategy.backend
user = backend.do_auth(access_token=token, backend=backend)
if user:
# login(request, user) #Only useful for web..
return user
else:
return None
and in the urls.py:
urlpatterns = patterns(
'',
url(r'^login/(?P<backend>[\w-]+)$', ObtainAuthToken.as_view(), ),
)
Sorry for attaching all this code and not providing a specific answer but more data is needed because the error can come from many sources (bad api keys, bad settings configuration, pipeline..). I hope the code helps.
I finally figured it out myself. According to this article in the Android's Google Plus documentation, I also need to request the plus.profile.emails.read scope when making the request in the Android app. Once I added this, the python-social-auth code managed to store the email properly in the uid fields. This allows it to recognize the same user whether logging in from the website or the app, which is what I needed. Here's the scopes string I use:
String scopes = "oauth2:" + Plus.SCOPE_PLUS_LOGIN + " https://www.googleapis.com/auth/plus.profile.emails.read";
However, the extra_data field still contains the values I mentioned above. I believe this is due to needing to request offline access as well, which would allow Google Plus to pass the missing fields back to python-django-auth. More details can be found here.
I've been running into the same problem. The reason why the extra_fields on your google user isn't being set is because python-social-auth calls the google server to set those things, but if you're calling Google with just an access_token, it won't be enough to get Google to return the refresh_token and all those other auth related fields. You can hack it by setting them manually, but then you'd end up using the same access and refresh tokens as the client. Google recommends that you use the client to generate a new authorization token with whatever scopes you need, and then send that auth token to the server, which then will turn it into an access and refresh token. See here for the details (it's a bit of an involved read): https://developers.google.com/identity/protocols/CrossClientAuth
If you're really committed to doing this in the scope of what python-social-auth does, I'd recommend making a custom auth backend, call it GoogleOAuth2AuthorizationCodeAuth (see here for details).
The lazier and probably easy-to-break and gross way is to post the access_token to my server to sign in as a google user (which you're doing properly, it seems), and then later, get another authorization token from the client in order to post to a separate endpoint, which I'll then handle turning into another Credentials model object that's connected to a user profile.
In DjangoRestFramework:
class GoogleAuthorizationCodeView(APIView):
def post(self, request, format=None):
credentials = flow.step2_exchange(code)
saved_creds = GoogleCredentials.objects.create(credentials=credentials)
return Response(status=status.HTTP_201_CREATED)