15

I'm trying to implement Google authentication in django with allauth and rest-auth. After many hours of research, none of the solutions I found worked in my project.

I based my code on a github issue: https://github.com/Tivix/django-rest-auth/issues/403

And an article: https://medium.com/@gonzafirewall/google-oauth2-and-django-rest-auth-92b0d8f70575

I have also created a Social Application object with Google client id and client secret

My project setup in google console:

Authorized JavaScript origins: http://localhost:8000 Authorized redirect URIs: http://localhost:8000/api/v1/users/login/google/callback/

My code

providers.py:

from allauth.socialaccount.providers.google.provider import GoogleProvider


class GoogleProviderMod(GoogleProvider):
    def extract_uid(self, data):
        return str(data['sub'])

adapters.py:

from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from google.auth.transport import requests
from google.oauth2 import id_token

from myproj.users.providers import GoogleProviderMod


class GoogleOAuth2AdapterIdToken(GoogleOAuth2Adapter):
    provider_id = GoogleProviderMod.id

    def complete_login(self, request, app, token, **kwargs):
        idinfo = id_token.verify_oauth2_token(token.token, requests.Request(), app.client_id)
        if idinfo["iss"] not in ["accounts.google.com", "https://accounts.google.com"]:
            raise ValueError("Wrong issuer.")
        extra_data = idinfo
        login = self.get_provider().sociallogin_from_response(request, extra_data)
        return login

views.py:

from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from rest_auth.registration.serializers import SocialLoginSerializer
from rest_auth.registration.views import SocialLoginView

from myproj.users.adapters import GoogleOAuth2AdapterIdToken


class GoogleLoginView(SocialLoginView):
    adapter_class = GoogleOAuth2AdapterIdToken
    callback_url = "http://localhost:8000/api/v1/users/login/google/callback/"
    client_class = OAuth2Client
    serializer_class = SocialLoginSerializer

urls.py:

from allauth.socialaccount.providers.oauth2.views import OAuth2CallbackView
from django.urls import path

from myproj.users.adapters import GoogleOAuth2AdapterIdToken
from myproj.users.views import GoogleLoginView

app_name = "users"
urlpatterns = [
    path(
        "login/google/",
        GoogleLoginView.as_view(),
        name="google_login"
    ),
    path(
        "login/google/callback/",
        OAuth2CallbackView.adapter_view(GoogleOAuth2AdapterIdToken),
        name="google_callback"
    ),
]

settings.py:

AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "allauth.account.auth_backends.AuthenticationBackend",
]
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "http"

When I passing ID Token (returned from Google 'sign in' button) as code parameter on login page, the error occurring:

allauth.socialaccount.providers.oauth2.client.OAuth2Error: Error retrieving access token: b'{\n "error": "invalid_grant",\n "error_description": "Malformed auth code."\n}' Response code is 400.

Actually, even if I passing some random text to the code, the error is the same.

Thanks for help!

PolishCoder
  • 170
  • 1
  • 1
  • 8
  • I figured out that passed 'code' was incorrect. I've got authorization code from Google OAuth 2.0 Playground. And now the error says about unauthorized_client. I read about it and it looks like I need to say in request that the 'code' was retrieved from oauth playground. How could I do that? I've tried to pass oauth playground url as 'redirect_uri' but it occurring the same error – PolishCoder Jul 29 '19 at 14:35

5 Answers5

6

I also integrate djangorestframework + django-allauth + django-rest-auth + djangorestframework-jwt. But, I just implement for Signin with Google, so user can't register manual.

Here is my code:

views.py

from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from rest_auth.registration.views import SocialLoginView


class GoogleLogin(SocialLoginView):
    adapter_class = GoogleOAuth2Adapter
    client_class = OAuth2Client

urls.py

...
path('auth/google', GoogleLogin.as_view(), name='google_login'),
...

settings.py

INSTALLED_APPS = [
    ...
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
    ...
]
SITE_ID = 1
REST_USE_JWT = True    # this is for djangorestframework-jwt

[UPDATE]
To use code params on SocialLoginView endpoint:

  • create redirect uri that accessible (I think it's on frontend, or you can use simple view)
  • use https callback (I use ngrok to do it in my local development)
  • access the url directly (dont use google oauth playgroud to get code, because the client_id is different) or just copy the request url on oauth palygroud and change the redirect_uri and cliend_id. here is the link https://accounts.google.com/o/oauth2/v2/auth?redirect_uri=<https_callback>&prompt=consent&response_type=code&client_id=<cliend_id>&scope=email&access_type=offline
aijogja
  • 199
  • 1
  • 2
  • 10
1

You don't need to customize the GoogleProvider class in your provider.py file.

Please add extra_data['id'] = extra_data['sub'] in complete_login function of your adapters.py. Then you can use GoogleProvider in your customized GoogleOAuth2Adapter adapter.

from google.oauth2 import id_token
from google.auth.transport import requests

from allauth.socialaccount.providers.oauth2.views import (
    OAuth2Adapter,
    OAuth2CallbackView,
    OAuth2LoginView,
)
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter

class GoogleOAuth2AdapterIdToken(GoogleOAuth2Adapter):

    def complete_login(self, request, app, token, **kwargs):
        idinfo = id_token.verify_oauth2_token(token.token, requests.Request(), app.client_id)
        if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
            raise ValueError('Wrong issuer.')
        extra_data = idinfo
        extra_data['id'] = extra_data['sub']
        login = self.get_provider() \
            .sociallogin_from_response(request,
                                       extra_data)
        return login

oauth2_login = OAuth2LoginView.adapter_view(GoogleOAuth2AdapterIdToken)
oauth2_callback = OAuth2CallbackView.adapter_view(GoogleOAuth2AdapterIdToken)

Please refer https://gonzafirewall.medium.com/google-oauth2-and-django-rest-auth-92b0d8f70575 and https://github.com/pennersr/django-allauth/issues/1983

NinjaDev
  • 1,228
  • 11
  • 21
0

Did you add the localhost address for the UI in the developers console for your project? I am trying to do something similar to your setup with Google authentication. I ended up creating another app within Django that is csrf exempt for one specific route, to register (update or create) a user when called with a valid token.

class VerifyToken(APIView):
     permission_classes = (AllowAny,)  # maybe not needed in your case
     authentication_classes = (UnsafeSessionAuthentication,)

@csrf_exempt
def post(self, request):
    print('hitting endpoint')
    requestJson = json.loads(request.body)
    request = requests.Request()

    id_info = id_token.verify_oauth2_token(requestJson['token'], request, settings.CLIENT_ID)

    if id_info['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
        return Response("Unable to Validate User: Wrong Issuer")
        # raise ValueError('Wrong issuer.')

    if not id_info:
        return Response("Unable to Validate User: Invalid Token")
        # raise Exception("Unable to Validate Token")

    id = id_info['email']
    user_letters = id_info['given_name'][0].upper() + id_info['family_name'][0].upper()
    # In this case, if the Person already exists, its name is updated
    user, created = User.objects.update_or_create(
        email=id, defaults={
            "first_name": id_info['given_name'],
            "last_name": id_info['family_name'],
            "last_login": datetime.datetime.now(),
            "email_verified": id_info['email_verified'],
            "exp": id_info['exp'],
            "locale": id_info['locale'],
            "name": id_info['name'],
            "picture": id_info['picture'],
            "initials": user_letters,
            "username": id_info['given_name'] + " " + id_info['family_name'],
        }
    )
    if created:
        serializer = UserSerializer(user)
        return Response(serializer.data)
0

i'm no sure about it but, your code have two errors.

First: Your adapter must have this lines

oauth2_login = OAuth2LoginView.adapter_view(GoogleOAuth2AdapterIdToken)
oauth2_callback = OAuth2CallbackView.adapter_view(GoogleOAuth2AdapterIdToken)

Second: You must add this line to your provider

provider_classes = [GoogleProviderMod]

And your app that contains the prover.py file must be added at the end of all the apps in the settings file, this is because there is a mapper with all the providers in your Django applications and in both of them (you provider and the provider from google) using the id of 'google'

I used two links you mention above, and i have some errors with them.

I know this has nothing to do with the doubt you mention, but I came to this post looking for how to integrate DRF with google Sign In, and this comment could be helped by someone else

0

Based on the answer by aijogja this was my solution. Using django-allauth and dj-rest-auth.

Added a callback_url to the REST Google login view:

class GoogleLogin(SocialLoginView):
    """Google login endpoint"""
    adapter_class = GoogleOAuth2Adapter
    client_class = OAuth2Client
    callback_url = 'http://localhost:8000/accounts/google/login/callback/'

This is the same URL that is added to the Google's API Console as a redirect URL. I use it for both standard Django login provided by allauth and the REST login view above.

Mapped this view to a google_token URL:

urlpatterns = [
    ....,
    path('google_token/', views.GoogleLogin.as_view(), name='google_token'),
    ....,
]

Since I was doing this for Android I might as well add that part as it may help someone:

GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestServerAuthCode(serverClientId)
            .requestEmail()
            .build();

...

GoogleSignInAccount account = completedTask.getResult(ApiException.class);
String authCode = account.getServerAuthCode();

Send this authCode to the endpoint defined above as a code value (the view accept code and authToken) and that's it. There should be a workflow when using the IdToken instead of an authCode but it seems that dj-rest-auth doesn't support that.

Mita_
  • 753
  • 1
  • 7
  • 10