iMerica / dj-rest-auth

Authentication for Django Rest Framework
https://dj-rest-auth.readthedocs.io/en/latest/index.html
MIT License
1.64k stars 306 forks source link

Update 2.2.6 broke Google login #465

Closed Altroo closed 1 year ago

Altroo commented 1 year ago

Trace : site-packages/dj_rest_auth/registration/serializers.py, line 150, in validate

-> login = self.get_social_login(adapter, app, social_token, token)

site-packages/dj_rest_auth/registration/serializers.py, line 60, in get_social_login

-> social_login = adapter.complete_login(request, app, token, response=response)

site-packages/allauth/socialaccount/providers/google/views.py, line 23, in complete_login

try: identity_data = jwt.decode( -> response["id_token"], …

Error : TypeError : string indices must be integers, not 'str'.

The response is not a dict but instead it's a string of some id which then i tried and got : OAuth2Error("Invalid id_token")

iMerica commented 1 year ago

PRs welcome. Please pin your version in your manifest and test before upgrading versions.

Altroo commented 1 year ago

PRs welcome. Please pin your version in your manifest and test before upgrading versions.

requirements.txt

Django==4.1.5 djangorestframework==3.14.0 django-allauth==0.52.0 dj-rest-auth==2.2.6

settings.py :

SOCIALACCOUNT_PROVIDERS = {
    'google': {
        'SCOPE': [
            'profile',
            'email',
        ],
        # in order to receive a refresh token on first login and on reauthentication requests
        # (which is needed to refresh authentication tokens in the background,
        # without involving the user’s browser)
        'AUTH_PARAMS': {
            'access_type': "offline",
        },
        'APP': {
            'client_id': config('GOOGLE_CLIENT_ID'),
            'secret': config('GOOGLE_SECRET'),
        },
    }
}

SOCIALACCOUNT settings

SOCIALACCOUNT_ADAPTER="account.adapters.BaseSocialAccountAdapter"
SOCIALACCOUNT_STORE_TOKENS=True
SOCIALACCOUNT_QUERY_EMAIL=True
SOCIALACCOUNT_EMAIL_VERIFICATION=False
SOCIALACCOUNT_AUTO_SIGNUP=True

Account settings

ACCOUNT_USERNAME_REQUIRED=False
ACCOUNT_AUTHENTICATION_METHOD="email"
ACCOUNT_EMAIL_REQUIRED=True
ACCOUNT_EMAIL_VERIFICATION="none"
ACCOUNT_MAX_EMAIL_ADDRESSES=1

BaseSocialAccountAdapter

from allauth.account.utils import perform_login
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
import logging
from account.models import CustomUser
logger = logging.getLogger(__name__)

class BaseSocialAccountAdapter(DefaultSocialAccountAdapter):
    def pre_social_login(self, request, sociallogin):
        user = sociallogin.user
        if user.id:
            return
        try:
            customer = CustomUser.objects.get(email=user.email)
            sociallogin.state['process'] = 'connect'
            perform_login(request, customer, 'none')
        except CustomUser.DoesNotExist:
            pass
    def get_app(self, request, provider, config=None):
        # NOTE: Avoid loading models at top due to registry boot...
        from allauth.socialaccount.models import SocialApp
        # 1 added line here
        from allauth.socialaccount import app_settings
        config = config or app_settings.PROVIDERS.get(provider, {}).get('APP')
        if config:
            app = SocialApp.objects.get_or_create(provider=provider)[0]
            for field in ["client_id", "secret", "key", "certificate_key"]:
                setattr(app, field, config.get(field))
        else:
            app = SocialApp.objects.get_current(provider, request)
        return app

    def authentication_error(self, request, provider_id, error=None, exception=None, extra_context=None):
        logger.warning('Facebook error! - provider id : {} - error : {} - exception : {} - extra_context : {}'
                       .format(provider_id, error.__str__(), exception.__str__(), extra_context))
NyllRE commented 1 year ago

same exact issue, this made me waste hours of debugging, please provide a solution so we could use it. I will try installing an old version

Altroo commented 1 year ago

same exact issue, this made me waste hours of debugging, please provide a solution so we could use it. I will try installing an old version

I think the issue isn't with the library, but maybe google api changed the response data.

NyllRE commented 1 year ago

installing an older version didn't work, is there a workaround for it? or at least another method of using google sign in as an api

Altroo commented 1 year ago

@NyllRE Could you please provide your config settings ?

NyllRE commented 1 year ago

Social Account + Account settings:


ACCOUNT_USERNAME_REQUIRED=True
# ACCOUNT_AUTHENTICATION_METHOD="email" #! this produces an error that stops the server
ACCOUNT_EMAIL_REQUIRED=False
ACCOUNT_EMAIL_VERIFICATION="none"
ACCOUNT_MAX_EMAIL_ADDRESSES=1
SOCIALACCOUNT_STORE_TOKENS=True
SOCIALACCOUNT_QUERY_EMAIL=True
SOCIALACCOUNT_EMAIL_VERIFICATION=False
SOCIALACCOUNT_AUTO_SIGNUP=True

social providers:

SOCIALACCOUNT_PROVIDERS = {
"google": {
    # For each OAuth based provider, either add a ``SocialApp``
    # (``socialaccount`` app) containing the required client
    # credentials, or list them here:
    "APP": {
        "client_id": "***",
        "secret": "***",
        "key": ""
    },
    # These are provider-specific settings that can only be
    # listed here:
    "SCOPE": [
        "profile",
        "email",
    ],
    "AUTH_PARAMS": {
        "access_type": "offline",
    }
}}

google login view:


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

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

urls:

    path('google/', GoogleLogin.as_view(), name='google_login'), # full url => auth/google/

when making a post request to auth/google/: image

Willem-Nieuwoudt commented 1 year ago

@NyllRE It seems like you are having the same issue as me: https://github.com/iMerica/dj-rest-auth/issues/467 I'm not sure if our issue is the same issue that @Altroo is having but I could be wrong.

NyllRE commented 1 year ago

@Willem-Nieuwoudt I originally had the same error that @Altroo had but when I played around trying to fix the issue it then produced this other error, so I think they are based on the same under-laying issue

Willem-Nieuwoudt commented 1 year ago

@NyllRE Alright. I personally haven't experienced @Altroo 's issue as of yet. Only that second issue you are facing now. Are you also only facing that second issue when posting an invalid token to the /auth/google endpoint? Everything works fine for me when I post a valid token, but it throws that error when I post the same token twice or any random invalid token. Not sure if you are doing this already but I have to decode the token that google gives me before posting it to /auth/google/. Im currently doing that with the decodeURIComponent('place_token_here') javascript function. It spits out a decoded version of the token. I then send that token to /auth/google/, which works for me.

NyllRE commented 1 year ago

this is my whole method of reproducing the issue

in my vue app I declare the script tag for the google auth:

const googleScript: any = document.createElement('script');
googleScript.setAttribute('src', 'https://accounts.google.com/gsi/client');
document.head.appendChild(googleScript);

I then declare a function that when pressed prompts you to sign in:

googleAuth = () => {
   // eslint-disable-next-line
   const client = google.accounts.oauth2.initCodeClient({
      client_id:  '***',
      scope: 'https://www.googleapis.com/auth/userinfo.profile',
      ux_mode: 'popup',
      callback: (response: { code: any }) => {
         if (response.code) {
            console.log(response.code); // I later tried to add the `decodeURIComponent()` function before logging the code, no difference
         }
      },
   });
   client.requestCode();
};

this logs the response code to the console

then I copy the code from the console to the drf page

image

doing so produces the error above

image

Willem-Nieuwoudt commented 1 year ago

Oh okay looks like our situation is a little different then, because I don't even have a frontend set up at all at this stage. My process to get the token is how they explain it in the docs: https://dj-rest-auth.readthedocs.io/en/latest/installation.html#social-authentication-optional

I essentially just go to the following url to get the token: https://accounts.google.com/o/oauth2/v2/auth?redirect_uri=CALLBACK_URL_YOU_SET_ON_GOOGLE&prompt=consent&response_type=code&client_id=YOUR_CLIENT_ID&scope=openid%20email%20profile&access_type=offline

Obviously I just replace the callback url and client_id with the relevant values. After that I just grab the token from the url and use that decodeURIComponent function on the token and post it to /auth/google. That has worked for me so far. Not so sure how the process would differ with a Vue frontend setup though.

NyllRE commented 1 year ago

my method basically does the same thing, I tried your method just to be sure and yea pretty much the same thing happens I still get the same error

alonw0 commented 1 year ago

Hi, this issue is caused by an update to the Django-allauth library. For easy and temporary fix you can downgrade it from 0.52 to 0.51 and it should work. Hope a fix will come out soon.

NyllRE commented 1 year ago

I downgraded and it didn't work, is there some sort of demo project that I can try to then compare what might be the reason I get this issue?

alonw0 commented 1 year ago

My response was to @Altroo issue... @NyllRE your issue is different I can try and help, Can you put your full urls file?

NyllRE commented 1 year ago

oh sorry I didn't realize. but thanks for your efforts, here's the file

base urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
...
    path('auth/', include('authapp.urls')),
...
]

authapp/urls.py

from django.urls import path, include
from .views import *
from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView
from dj_rest_auth.registration.views import (
    SocialAccountListView, SocialAccountDisconnectView
)

urlpatterns = [
    path('', Home),
    path('users/', viewUsers.as_view()),

    path('lib/', include('dj_rest_auth.urls')),
    path('lib/registration/', include('dj_rest_auth.registration.urls')),
    path('facebook/', FacebookLogin.as_view(), name='fb_login'),
    path('google/', GoogleLogin.as_view(), name='google_login'),
    path('google/connect/', GoogleConnect.as_view(), name='google_connect'),
    path(
        'socialaccounts/',
        SocialAccountListView.as_view(),
        name='social_account_list'
    ),
    path(
        'socialaccounts/<int:pk>/disconnect/',
        SocialAccountDisconnectView.as_view(),
        name='social_account_disconnect'
    ),
    path('token/', UserLogIn.as_view(), name='token_optain_more'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),

    path('register/', RegisterView.as_view(), name='auth_register'),
]

views.py:

from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.oauth2.views import OAuth2LoginView

...

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

pip freeze:

asgiref==3.6.0
certifi==2022.12.7
cffi==1.15.1
charset-normalizer==2.1.1
crispy-bootstrap5==0.7
cryptography==39.0.0
defusedxml==0.7.1
dj-rest-auth==2.2.4
Django==4.1.4
django-allauth==0.50.0
django-cors-headers==3.13.0
django-crispy-forms==1.14.0
django-rest-framework==0.1.0
djangorestframework==3.14.0
djangorestframework-simplejwt==5.2.2
idna==3.4
oauthlib==3.2.2
pycparser==2.21
PyJWT==2.6.0
python3-openid==3.2.0
pytz==2022.7
requests==2.28.1
requests-oauthlib==1.3.1
six==1.16.0
social-auth-app-django==5.0.0
social-auth-core==4.3.0
sqlparse==0.4.3
urllib3==1.26.13

tried django-allauth 0.51 then tried 0.50 to see if there's any difference but no difference lol, I also tried older dj-rest-auth versions to see if it might be resolved with older versions but it still persists

NyllRE commented 1 year ago

I might have found something that could solve the issue,

my issue that came after the op's issue

I read the source code of django-allauth and found that the issue I was getting in here was because the access token was empty, apparently this code exists inside the source code:

allauth/socialaccount/providers/oauth2/client.py, line 91, in get_access_token:

if not access_token or "access_token" not in access_token:
    raise OAuth2Error("Error retrieving access token: %s" % resp.content)

the issue is that the url that dj-rest-auth documentation only returns the code but not the access token, but as you can see the code checks if there is an access token or if the string "access_token" in inside the access_token string. so I tried just adding "access_token" and it worked

the op's issue

after filling it in I got the original issue string indices must be integers and surfed the code again, turns out the issue was because since me and the op only posted either the access token or the code, the response returned was a string instead of an object. if you inspect the GoogleOAuth2Adapter class you will find that the function complete_login is trying to return identity data in this way image

this apparently was the reason we got this error: image

the route displayed on the error is different because I copied the function to modify it to get different results

so I tried to check the reason we were getting this error by printing the response value and type. turns out printing response and type(response) returns access_token <class 'str'> which means that response was for some reason getting the access_token value and GoogleOAuth2Adapter was trying to extract id_token from that string

I dug deep to find the source of why it was giving a string to the response and found this image

for some reason the comment writes OAuth1, this is on version 2.2.6. weird but I guess it might refer to something else

at this point I tried digging deeper but I found that it was getting too complicated for me at this point. I hope this could be enough to find the source of the issue

henningbra commented 1 year ago

Could it be related to my issue in the upgrade of django-allauth from 0.51.0 to 0.52.0?

Issue: https://github.com/pennersr/django-allauth/issues/3228

Code owner believes the problem is related to a old issue in dj_rest_auth that now resurfaced.

alonw0 commented 1 year ago

@henningbra Yes, the main issue here is the same of what you provided.

@NyllRE You have a bit of a mix, I'll try to explain. There are 2 main types of Google oauth flows used here:

  1. The access token method where your front will get the access token and Id token and send it to the backend to login/register. This is where access token is used and this is what got broken with the change in allauth 0.52.0 behavior.

  2. The code method where you get only a code in your front end and the Django check it and verify it for you, making it a bit more secure (Some will disagree). The code is good only for one use and then it goes off.

The reason you get invalid grant "bad request" can be caused by many reasons from issues in your config of the Google app to clock setting issues. (See here: https://blog.timekit.io/google-oauth-invalid-grant-nightmare-and-how-to-fix-it-9f4efaf1da35)

My best guess is google app conf issues or that allauth is using your code to verify it and when you run it again it's already bad...

Try removing allauth urls ("/accounts/") and see if it changes - you will get an error because callback url won't work but take the code from there

wanglophile commented 1 year ago

Anyone have any update or luck with solving this?

Or at least narrowed down what the problem is? (On dj-rest-auth's side? Django-allauth's side? Google's API?)

I've tried reverting back a bunch of dj-rest-auth and django-allauth's but getting the same errors.

Altroo commented 1 year ago

@wanglophile @alonw0 @NyllRE @henningbra @iMerica Fix : revert to those versions, tested it & it worked again. django-allauth==0.51.0 dj-rest-auth==2.2.5

Altroo commented 1 year ago

The issue is in allauth to fix with last versions:

allauth/socialaccount/providers/google/views.py should be :

import requests

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

from .provider import GoogleProvider

class GoogleOAuth2Adapter(OAuth2Adapter):
    provider_id = GoogleProvider.id
    access_token_url = "https://accounts.google.com/o/oauth2/token"
    authorize_url = "https://accounts.google.com/o/oauth2/auth"
    profile_url = "https://www.googleapis.com/oauth2/v1/userinfo"

    def complete_login(self, request, app, token, **kwargs):
        resp = requests.get(
            self.profile_url,
            params={"access_token": token.token, "alt": "json"},
        )
        resp.raise_for_status()
        extra_data = resp.json()
        login = self.get_provider().sociallogin_from_response(request, extra_data)
        return login

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

allauth/socialaccount/providers/google/provider.py should be :

...
class GoogleProvider(OAuth2Provider):
    def extract_uid(self, data):
        return str(data["id"]) // <--
...
toniengelhardt commented 1 year ago

Downgrading allauth to 0.51.0 solved it for me.

Why is the issue closed, will/should this be fixed in allauth and is there an issue for it to track?

toniengelhardt commented 1 year ago

Is this still an issue with allauth 0.53.1 ?

henningbra commented 1 year ago

Seems to be fixed, but have upgraded yet, can't confirm.

On Sun, 26 Mar 2023, 13:11 Toni Engelhardt, @.***> wrote:

Is this still an issue with allauth 0.53.1 ?

— Reply to this email directly, view it on GitHub https://github.com/iMerica/dj-rest-auth/issues/465#issuecomment-1484064002, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABL24SNB2HJAX3DU4FDJVILW6AXA3ANCNFSM6AAAAAATRALVQU . You are receiving this because you were mentioned.Message ID: @.***>