[Solved]-Python-social-auth not getting correct Google OAuth2 details

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.

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

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.

👤argaen

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)

Leave a comment