Re-implementing Social Logins (in Django) - django

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'),
]

Related

Setup google login using Django rest framework plus React Js

views
class GoogleCreateAccount(APIView):
permission_classes = [permissions.AllowAny]
def post(self, request):
reg_serializer = RegisterSerializer(data=request.data)
if reg_serializer.is_valid():
new_user = reg_serializer.save()
if new_user:
# add these
r = requests.post('http://127.0.0.1:8000/auth/token', data={
'username': new_user.email,
'id_token': request.data['code'],
#'password': request.data['password'],
'client_id': '304129974707-79jslq7l318va16eacni8cveokn237g8.apps.googleusercontent.com',
'client_secret': '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',
'grant_type': 'id_token'
})
return Response(r.json(), status=status.HTTP_201_CREATED)
return Response(reg_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
urls.py
urlpatterns = [
path('auth/', include('drf_social_oauth2.urls',namespace='drf')),
path('googleregister/',GoogleCreateAccount.as_view()),
]
I was setting up an google login with django and react app i was setting it up based on the https://abhik-b.medium.com/step-by-step-guide-to-email-social-logins-in-django-5e5436e20591 above link. It is working on the bcakend as expected. But for getting the access token the parameters I giving is
{
clientid:
client secret:
grant type: password
username:
password:
}
But in the react it wasn't able to get the password parameter so it is not getting connected to the rest api call. I don't know exactly how to get this issue sorted. Please suggest me a best link or docs for setting up google login for django rest framework as backend and react js as frontend.
And is this possible to change the grant type to authorisation code and if so what needs to be changed?

Django+React: What is the bet way to properly implement authentication and security on API requests from React to Django backend

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.

Python Social Auth - User is authenticated with facebook but cannot access pages

I have an issue with Facebook authentication with Python Social Auth.
I have login with Facebook, Google and Twitter.
After login, I redirect the user to my dashboard at /user/dashboard with the use of login_redirect decorator. While it works fine with Google and Twitter, I am not able to redirect the user authenticated with Facebook.
#login_required
def home(request):
user = ""
if '_auth_user_id' in request.session:
user = AuthUser.objects.get(id=request.session['_auth_user_id'])
template = 'user/index.html'
return render(request, template, context)
In Settings.py
SOCIAL_AUTH_FACEBOOK_KEY = '******'
SOCIAL_AUTH_FACEBOOK_SECRET = '*******'
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email', 'public_profile', 'user_location']
SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {
'locale': 'en_US',
'fields': 'id, name, email, age_range, about, picture, location'
}
SOCIAL_AUTH_FACEBOOK_API_VERSION = '2.10'
When I remove the login_required decorator, the user is redirected to the dashboard. But when the user tries to go to another page, there django says user is not authenticated. Is this an issue with the Facebook API or the application?
Thanks for any replies.
1) Check AUTHENTICATION_BACKENDS. Facebook authentication backend must be in this list.
2) Cleanup cookies and check that facebook user is_active on you site.
Here's a quick and dirty fix. I didn't look at all possible scenarios. This answer can be improved. First step is to get rid of the login required decorator from the redirect view. Then use the following code in the view
if request.user.is_anonymous():
# check if user logged in through facebook, csrf token will be validated by the middleware
if '_auth_user_id' in request.session:
auth_user_id = request.session['_auth_user_id']
user_obj = User.objects.filter(id=auth_user_id)
request.user = user_obj[0]
userProfile = model_to_dict(user_obj[0])
else:
# redirect user to login page
return HttpResponseRedirect('/login/')
You may have to update your app permission to provide the desired pieces of information(including email).
Go to https://developers.facebook.com/tools/explorer/, select your app and the permission you want to provide. Then generate a new test access token.

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)

django-allauth Not Saving Social Info

So I'll give full disclosure from the get-go that I am quite new to both Django and django-allauth.
Now that that is out of the way, the problem that I am having is that when a user logs in via a social site, (I have been trying Google and Facebook), none of the data retrieved from the site is pulled into the user's data fields. After authenticating, the user is still prompted to enter an email, and all name fields are left blank. I tried to fix this manually by creating a custom adapter, but that did not work either. From using print statements, I can see that the data is being fetched from the site just fine -- it just isn't being saved to the user's attributes.
Correct me if I'm wrong, but by reading the documentation and the some of the source of django-allauth, I am under the impression that social authorization automatically saves the user's email and first and last names via the populate_user(self, request, sociallogin, data): hook in the DefaultSocialAccountAdapter class, so I really shouldn't even have to deal with workarounds.
Thus, I'm guessing that I am just doing something foolish that is messing this up for me... Although if there is a clever workaround that will fix this problem, I'd be fine with that, for lack for a better solution.
Note: Using Django 1.7 and Python 3.4.1
EDIT: Django-allauth is succeeding in creating a User and linking the user to a social account, which contains all of the data fetched from the social site, but none of that data is populating the fields within the User object, like email, first_name, and last_name.
Here are my django-allauth configuration settings in settings.py:
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = "required"
ACCOUNT_USERNAME_REQUIRED = False
SOCIALACCOUNT_AUTO_SIGNUP = True
# The following line was uncommented when I was trying to use my own adapter
# SOCIALACCOUNT_ADAPTER = 'profiles.profile_adapter.ProfileAdapter'
SOCIALACCOUNT_PROVIDERS = {
'facebook':
{ 'SCOPE': ['email'],
'AUTH_PARAMS': {'auth_type': 'reauthenticate'},
'METHOD': 'oauth2',
'LOCALE_FUNC': lambda request: 'en_US'},
'google':
{ 'SCOPE': ['https://www.googleapis.com/auth/userinfo.profile'],
'AUTH_PARAMS': { 'access_type': 'online' } },
}
And here is the code I had in my custom adapter (Which, by using print statements, I could tell was getting used and processing the correct data) where I tried to manually save the fields into the user object
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class ProfileAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
'''
Check for extra user data and save the desired fields.
'''
data = sociallogin.account.extra_data
user = sociallogin.account.user
print("LOGS: Caught the signal -> Printing extra data of the account: \n" + str(data))
if 'first_name' in data:
user.first_name = data['first_name']
elif 'given_name' in data:
user.first_name = data['given_name']
if 'last_name' in data:
user.last_name = data['last_name']
elif 'family_name' in data:
user.last_name = data['family_name']
user.save()
Note The above code creates a user in the database that is not linked to any social account, but contains the correct first and last names. Then the user is redirected to a form saying they are logging in with a social account and is prompted for an email address. Once this form is submitted, the original user created is overwritten by a new user that is linked to a social account, contains the email entered into the form, but does not have first or last name fields populated.
The problem was that when an email was not included with the data fetched from the social media site, django-allauth would ask for an email in a subsequent form to create the account with. When the account is then created from this form, django-allauth would not use the data fetched from the social media to populate fields. I think that this is a problem with django-allauth.