Python Social Auth, Google, and refresh token - python

In a personal project, I am trying to use Django as my front end and then allow data entered by users in a particular form to be copied to google sheets.
Google's own docs recommend using https://github.com/google/oauth2client which is now deprecated, and the docs have not been updated. With this, I have started attempting to use Python Social Auth and Gspread. For Gspread to be able to function correctly, I need to be able to pass it not only an access token but also a refresh token. Python Social Auth however is not persisting the refresh token along with the rest of the "extra data". Looking at the data preserved and the URLs routed to, it seems to me more like somewhere it is routing through Google+.
I have the following configurations in my Django settings files:
AUTHENTICATION_BACKENDS = (
'social_core.backends.google.GoogleOAuth2',
'django.contrib.auth.backends.ModelBackend',
)
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
'social_core.pipeline.social_auth.associate_by_email',
)
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = '...'
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = '...'
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['https://www.googleapis.com/auth/spreadsheets']
Is there a better way to access a google sheet?
Am I correct that PSA or google is redirecting me into a Google+ auth flow instead of the Google Oauth2?
If not, what must change so that Python Social Auth keeps the refresh token?

It's true that python-social-auth will use some bits of the Google+ platform, at least the API to retrieve details about the user to fill in the account.
From your settings, I see you have associate_by_email at the bottom, at that point, at that point it has no use since the user is already be created, if you really plan to use it, it must be before the create_user one, you can check the DEFAULT_PIPELINE as a reference.
In order to get a refresh_token from google, you need to tell it that you want one, to do that you need to set the offline access type:
SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = {
'access_type': 'offline'
}
With that setting Google will give you a refresh_token and it will automatically stored in extra_data.

Just provide this in your settings.py:
SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = {
'access_type': 'offline',
'hd': 'xyzabc.com',
'approval_prompt':'force'
}
remeber there is {'approval_prompt' : 'force'} which will force the user to select the gmail account, this way you will get refresh token.

You can send extra parameters to the OAuth2 provider using the variable
SOCIAL_AUTH_<PROVIDER>_AUTH_EXTRA_ARGUMENTS
For Google, you can see the extra parameters they accept in their documentation (scroll down to "parameters"). The one we are looking for is access_type:
access_type: Indicates whether your application can refresh access tokens when the user is not present at the browser. Valid parameter values are online, which is the default value, and offline.
So we can add the following to settings.py, to indicate that we want to receive a refresh token:
SOCIAL_AUTH_GOOGLE_OAUTH2_EXTRA_ARGUMENTS = {"access_type: offline"}
The results from EXTRA_ARGUMENTS will be stored in extra_data, so the refresh token can be accessed like this:
refresh_token = user.social_auth.get(provider="google-oauth2").extra_data["refresh_token"]
One possible solution is to store the refresh token alongside the user in a UserProfile model, by adding a custom function to the social-auth pipeline:
Create the model
# models.py
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
refresh_token = models.CharField(max_length=255, default="")
Add a function to access store the refresh token
# pipeline.py
from .models import UserProfile
def store_refresh_token(user=none, *args, **kwargs):
extra_data = user.social_auth.get(provider="google-oauth2").extra_data
UserProfile.objects.get_or_create(
user=user, defaults={"refresh_token": extra_data["refresh_token"]}
)
Add our new function to the social-auth pipeline.
# settings.py
...
SOCIAL_AUTH_PIPELINE = (
...
"my-app.pipeline.store_refresh_token"
)
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
'https://www.googleapis.com/auth/spreadsheets'
# any other scopes you need
]
...
The token is now stored alongside the user and can be used to initialise the sheets client or whatever else you need.

Related

Python Social Auth with Django - how to retrieve extra data?

For my first Django project I wanted to tie up Python Social Auth for the social authentication (namely Facebook).
Django==2.0
social-auth-app-django==2.1.0
social-auth-core==1.7.0
How can I retrieve extra data from the logged in user's profile? My goal is to filter the logged in users to custom groups based on FB groups they are members of. However at this point I can't even get the email, just the username.
In my settings.py:
SOCIAL_AUTH_FACEBOOK_KEY = 'xxxx'
SOCIAL_AUTH_FACEBOOK_SECRET = 'xxxx'
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email', 'groups_access_member_info']
SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {
'fields': 'id, name, email',
'edges': 'groups'
}
The pipeline is the basic pipline:
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.auth_allowed',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
)
I'm not asking for complete code, any help would be much appreciated.
Thank you in advance!
python-social-auth will just store the basic user information it needs to fill the model fields, if anything extra is part of the auth payload from the provider, and it's configured in the EXTRA_DATA setting, it will also be stored in the social-related class as part of the extra_data attribute.
Still, python-social-auth won't call any other API in the provider to fetch additional data, for that to work, you need to enhance the PIPELINE with your methods that will call these additional endpoints on Facebook, once with the response, you can store it were it fits on your project.
To debug what's coming by default from the provider, add the debug pipeline between the steps (social_core.pipeline.debug.debug). If what you are looking for is already part of the payload, then take note of the key name and add it to the EXTRA_DATA setting. If it's not, then you need to add a method that will call Facebook API to retrieve the extra information.

Django Social Auth Pipeline-- Checking for Duplicate Emails

So i'm piggybacking on this post here:
Python Social Auth duplicating e-mails for different users
Here's my issue:
I provide a user with ability to signup either via regular email sign up, facebook auth or twitter auth.
I'm also using same package Social Django Auth App for the user login pages.
I realized that a user might try sign up with a Facebook account (associated to one email) and then try again later to register with Twitter (which could have the same email address). Based on the aforementioned post, I added a function to check for duplicate emails like so:
def check_email_exists(request, backend, details, uid, user=None, *args, **kwargs):
email = details.get('email', '')
provider = backend.name
# check if social user exists to allow logging in (not sure if this is necessary)
social = backend.strategy.storage.user.get_social_auth(provider, uid)
# check if given email is in use
count = User.objects.filter(username=email).count()
success_message = messages.success(request, 'Sorry User With That Email Already Exists')
# user is not logged in, social profile with given uid doesn't exist
# and email is in use
if not user and not social and count:
return HttpResponseRedirect(reverse('accounts:sign_up', success_message))
and my pipeline with the function:
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.auth_allowed',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'dealmazing.utils.check_email_exists',
'social_core.pipeline.social_auth.associate_by_email', # <--- enable this one
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
)
UPON Testing--if i go to sign up with Twitter account of an already registered email address--it works. YAY!
BUT the main issue comes when i go to try to login via using either Facebook or Twitter. The function is checking on those logins as well and spitting me back the 'Email Allready Exists..' error.
So I somehow need to decipher between a login and a registration, but I'm having trouble finding how i actually do this with the social auth package.
Any ideas?
The difference between login and register is up to your project, it looks like in your scenario, a user landing in your pipeline function that matches an email in your DB should be considered like a login attempt and not a new singup. That's basically the functionality of associate_by_email method.
You might see the potential risk in this when a user uses a service that doesn't validate the email addresses, they can take control over somebody else account. You can mitigate this by doing validation on your end after any new social account is added, or by trusting services that are known to validate emails on their ends.
I would say, that you have to remove from the pipeline
'social_core.pipeline.social_auth.associate_by_email'
Reason: your application is not supporting unique user emails in User data model, and you are getting yourself into trouble. If you are not verifying user emails, then the trouble might be even bigger.

Passing values from HTML to python using EVE rest framework

I am creating a website using html as a frontend and python as a backend using EVE framework. I have enabled token authentication for my usersRESTful Account Management. But when I pass the values to the EVE framework it gives me a 401.
var login = function (loginData) {
var deferred = $q.defer();
$http.post(appConfig.serviceUrl + 'user',{data:loginData})
here the loginData holds the username and password of my user from the html page this piece of code is inside a .js file.
My api.py holds the following authentication code.
class RolesAuth(TokenAuth):
def check_auth(self, token, allowed_roles, resource, method):
# use Eve's own db driver; no additional connections/resources are used
accounts = app.data.driver.db['user']
lookup = {'token': token}
if allowed_roles:
lookup['roles'] = {'$in': allowed_roles}
account = accounts.find_one(lookup)
return account
def add_token(documents):
# Don't use this in production:
# You should at least make sure that the token is unique.
for document in documents:
document["token"] = (''.join(random.choice(string.ascii_uppercase)
for x in range(10)))
My problem is as soon as the api.py is run it asks to provide proper credentials. How can i send the token directly to the auth mechanism so that it lets me access the db.
How will you suggest me to get rid of the authentication alert box.
I want the token to be automatically sent to the api.
If suppose I use basic authentication how can I send the username and password values directly and validate it? Without having the browser pop-up box asking for username and password
Thanks in advance.
Does it work with curl ? Refer to this question
Also, refer to this and this thread on the mailing list.

Unable to refresh a Google OAuth 2 token after update using 'Python Social Auth'

I am using Python Social Auth in a Django project and everything was working fine.
I have just updated Python social authentication to 0.2.1, and I am getting an error with load_strategy when trying to refresh a Google OAuth 2 token.
Until now I was using:
strategy = load_strategy(backend='google-oauth2')
user = UserSocialAuth.objects.get(uid=..., provider="google-oauth2")
refresh_token(strategy=strategy, redirect_uri='http://.....')
Now I am getting this error:
TypeError: load_strategy() got an unexpected keyword argument 'backend'
I was experiencing the same issue after update.
Here is example from the docs:
It’s a common scenario that mobile applications will use an SDK to signup a user within the app, but that signup won’t be reflected by python-social-auth unless the corresponding database entries are created. In order to do so, it’s possible to create a view / route that creates those entries by a given access_token. Take the following code for instance (the code follows Django conventions, but versions for others frameworks can be implemented easily):
from django.contrib.auth import login
from social.apps.django_app.utils import psa
# Define an URL entry to point to this view, call it passing the
# access_token parameter like ?access_token=<token>. The URL entry must
# contain the backend, like this:
#
# url(r'^register-by-token/(?P<backend>[^/]+)/$',
# 'register_by_access_token')
#psa('social:complete')
def register_by_access_token(request, backend):
# This view expects an access_token GET parameter, if it's needed,
# request.backend and request.strategy will be loaded with the current
# backend and strategy.
token = request.GET.get('access_token')
user = request.backend.do_auth(request.GET.get('access_token'))
if user:
login(request, user)
return 'OK'
else:
return 'ERROR'
Hope that helps

How to update a user's `extra_data` after they have been associated with account?

I've successfully managed to use django-socialauth to associate an account (in this case, an instagram account) with an existing user account. I've also set up my pipeline to collect additional user details:
def update_social_auth(backend, details, response, social_user, uid, user,
*args, **kwargs):
if getattr(backend, 'name', None) in ('instagram', 'tumblr'):
social_user.extra_data['username'] = details.get('username')
social_user.save()
This works great when an account is associated for the first time. However, if the account has already been associated, the username field will not be present in extra_data.
How can I update a user's extra_data after the association has already been made? Is there a way using django-socialauth to do this without disconnecting and reconnecting, or using the account's (e.g Instagram's) API?
If it helps, this is my pipeline at the moment:
SOCIAL_AUTH_PIPELINE = (
'social_auth.backends.pipeline.social.social_auth_user',
'social_auth.backends.pipeline.social.associate_user',
'social_auth.backends.pipeline.social.load_extra_data',
'social_auth.backends.pipeline.user.update_user_details',
'apps.utils.social.utils.update_social_auth'
)
Here is a snippet of code I use to add 'admin' and 'staff' options to an existing Django user; I don't know about django-socialauth or the extra_data field, but I'm guessing something like this might be applicable:
:
userqueryset = User.objects.filter(username=user_name)
if not userqueryset:
print("User %s does not exist"%user_name, file=sys.stderr)
return am_errors.AM_USERNOTEXISTS
# Have all the details - now update the user in the Django user database
# see:
# https://docs.djangoproject.com/en/1.7/ref/contrib/auth/#django.contrib.auth.models.User
# https://docs.djangoproject.c om/en/1.7/ref/contrib/auth/#manager-methods
user = userqueryset[0]
user.is_staff = True
user.is_superuser = True
user.save()
:
FWIW, my app is using 3rd party authentication (specifically atm OpenId Connect via Google+), so I think there's some common goal here. In my case I want to be able to add Django admin privileges to a user that has already been created.
The full module containing the above code is at github.com/gklyne/annalist/blob/develop/src/annalist_root/annalist_manager/am_createuser.py#L231

Categories