python-social-auth not getting correct Google OAuth2 details
Asked Answered
L

4

12

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.

Lam answered 30/1, 2015 at 14:21 Comment(1)
hi can you share full code pleaseEnvironment
L
1

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.

Lam answered 6/2, 2015 at 19:39 Comment(1)
Even though I figured it out, the bounty is still open to anyone who can detail on populating the extra_data field.Lam
F
5

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
Fleda answered 8/2, 2015 at 22:7 Comment(1)
What should I import Profile?Trailblazer
W
4

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.

Washable answered 5/2, 2015 at 22:48 Comment(2)
From what I see we're doing pretty much the same things ... Is your code, when receiving a request from something other than a browser, working? Do you get the correct details in the social_auth_usersocialauth table? The keys should be fine as the website login works fine. The only thing you have extra is the pipelines. I am not specifying any pipelines. Could this be the issue?Lam
Yes, it is working for api request (as I said, I'm using django rest framework for the mobile client part). It could be one of the reasons. The try may worth it. And yes, I was getting the correct user into the user table when some1 registered with Google.Washable
L
1

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.

Lam answered 6/2, 2015 at 19:39 Comment(1)
Even though I figured it out, the bounty is still open to anyone who can detail on populating the extra_data field.Lam
C
0

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)
Cruce answered 28/5, 2015 at 18:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.