Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

OAuth2Provider.get_scope() takes 1 positional argument but 2 were given #639

Closed
shennnj opened this issue May 31, 2024 · 15 comments
Closed

Comments

@shennnj
Copy link

shennnj commented May 31, 2024

Getting this error when sending a post request to SocialLoginView. The body of post request contains "code" only. Having this problem on google/facebook/github login.

Similar problem also asked in https://stackoverflow.com/questions/78477908/dj-rest-auth-with-google-login-typeerror-oauth2provider-get-scope-takes-1-po

Did I do any mistake in setting this up?

The error:
Happens during validation in SocialLoginSerializer

  File "/home/shen/.local/lib/python3.12/site-packages/rest_framework/views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/shen/.local/lib/python3.12/site-packages/dj_rest_auth/views.py", line 125, in post
    self.serializer.is_valid(raise_exception=True) <br>
  File "/home/shen/.local/lib/python3.12/site-packages/rest_framework/serializers.py", line 223, in is_valid
    self._validated_data = self.run_validation(self.initial_data)
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
  File "/home/shen/.local/lib/python3.12/site-packages/rest_framework/serializers.py", line 445, in run_validation
    value = self.validate(value)
            ^^^^^^^^^^^^^^^^^^^^ 
  File "/home/shen/.local/lib/python3.12/site-packages/dj_rest_auth/registration/serializers.py", line 122, in validate
    scope = provider.get_scope(request)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^

view.py

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
    client_class = OAuth2Client

    @property
    def callback_url(self):
        return f"http://localhost:3000/"

url.py

from django.urls import path, include
from . import views
from .views import GoogleLogin, google_callback
from allauth.socialaccount.providers.google import views as google_views

urlpatterns = [
    path("auth/", include("dj_rest_auth.urls")),
    path("auth/google/url/", google_views.oauth2_login, name="google_auth"), # redirect to google authentication page
    path("auth/google/callback/", google_callback, name="google_callback"), # callback from authentication page, record the authorization code
    path("auth/google/", GoogleLogin.as_view(), name="google_login"), # use authorization code to login
]

views.py

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
    client_class = OAuth2Client

    @property
    def callback_url(self):
        return f"http://localhost:3000/"

from django.shortcuts import redirect
def google_callback(request):
    return redirect(f"http://localhost:3000/")

Post request
Post to /auth/google/ with body of
{ "code": "<code_received_after_user_authorize>" }

@shennnj
Copy link
Author

shennnj commented Jun 1, 2024

Seems like django-allauth update to 0.62.0 changes how get_scope is implemented, will downgrade django-allauth to 0.61.1 at the moment to have dj-rest-auth work together.

https://github.com/pennersr/django-allauth/blob/0.61.1/allauth/socialaccount/providers/oauth2/provider.py

    def get_scope(self, request):
        settings = self.get_settings()
        scope = list(settings.get("SCOPE", self.get_default_scope()))
        dynamic_scope = request.GET.get("scope", None)
        if dynamic_scope:
            scope.extend(dynamic_scope.split(","))
        return scope

https://github.com/pennersr/django-allauth/blob/0.62.0/allauth/socialaccount/providers/oauth2/provider.py

    def get_scope(self):
        """
        Returns the scope to use, taking settings `SCOPE` into consideration.
        """
        settings = self.get_settings()
        scope = list(settings.get("SCOPE", self.get_default_scope()))
        return scope

    def get_scope_from_request(self, request):
        """
        Returns the scope to use for the given request.
        """
        scope = self.get_scope()
        dynamic_scope = request.GET.get("scope", None)
        if dynamic_scope:
            scope.extend(dynamic_scope.split(","))
        return scope

@Te0SX
Copy link

Te0SX commented Jun 30, 2024

My setup with django-allauth==0.61.1 and dj-rest-auth==5.0.2 was working fine for like months. Before a few days ago I started getting errors like the above and "allauth.socialaccount.providers.oauth2.client.OAuth2Error: Invalid id_token".

Check this reply by one of the main dev of allauth: #503 (comment)

Going back to django-allauth==0.57.1 solved my issues.

@ThukuWakogi
Copy link

ThukuWakogi commented Jul 7, 2024

I came across this issue while using Github as a provider and @shennnj's solution worked for me.

However, while using dj-rest-auth 6.0.0 and django-allauth 0.63.2, I noticed that the client class in dj-rest-auth is being instantiated with an extra scope argument.

in the validate method of SocialLoginSerializer, the client is instantiated with an extra scope argument.

provider = adapter.get_provider()
scope = provider.get_scope(request)
client = self.client_class(
request,
app.client_id,
app.secret,
adapter.access_token_method,
adapter.access_token_url,
self.callback_url,
scope,
scope_delimiter=adapter.scope_delimiter,
headers=adapter.headers,
basic_auth=adapter.basic_auth,
)

This is not needed in the instantiation of a new client class

https://github.com/pennersr/django-allauth/blob/40117a711746be888528af69029cc5ed2692a7b2/allauth/socialaccount/providers/oauth2/client.py#L13-L38

So to address this problem, I inherited SocialLoginSerializer and removed the scope argument

class CstmSocialLoginSerializer(SocialLoginSerializer):
    def validate(self, attrs):
            ...
            client = self.client_class(
                request,
                app.client_id,
                app.secret,
                adapter.access_token_method,
                adapter.access_token_url,
                self.callback_url,
                scope_delimiter=adapter.scope_delimiter,
                headers=adapter.headers,
                basic_auth=adapter.basic_auth,
            )
            ...

Then added the serializer to my GithubLoginView

class GitHubLogin(SocialLoginView):
    adapter_class = GitHubOAuth2Adapter
    callback_url = "..."
    client_class = OAuth2Client
    serializer_class = CstmSocialLoginSerializer

This solved my problem and I didn't get the error.

@YDA93
Copy link

YDA93 commented Jul 14, 2024

We are encountering this issue with Apple login despite it previously functioning correctly.

@YDA93
Copy link

YDA93 commented Jul 14, 2024

We are encountering this issue with Apple login despite it previously functioning correctly.

Downgrading to django-allauth to 0.61.1 Fixes the issue.

@trackers153
Copy link

@YDA93 - could you share your dj_rest_auth version that works with django-allauth 0.61.1 for social auth via Apple?

@YDA93
Copy link

YDA93 commented Aug 28, 2024

@trackers153 Sure dj-rest-auth==6.0.0

@trackers153
Copy link

Thanks vm, @YDA93

@toniengelhardt
Copy link

Not fixed in 7.0.0 ? 😩

@joeychrys
Copy link

any solutions? I'm unable to downgrade now since one of the latest PR. Idk which one but I think someone made django-allauth > 64.0.1 or something like that.

@joeychrys
Copy link

joeychrys commented Dec 10, 2024

Hi @toniengelhardt & everyone. Just wanted to share this solution if anyone is still having problems. I'm happy to help so feel free to message me via the info on my page.

My goal:
Front-end: Sends Google Authorization Code
→ Django: Receives Code, Sends it to Google
→ Google OAuth: Returns Access Token & User Details to Django
→ Django: Creates/Fetches User, Sets JWT Access & Refresh Cookies
→ Front-end: Now has a persistent, authenticated session

Versions:

django==5.0.9 dj-rest-auth==7.0.0 django-allauth[mfa]==65.0.2

Step 1: Remove scope from dj-rest-auth social login serializer when utilizing an authorization code.

from django.contrib.auth import get_user_model
from django.db import IntegrityError
from django.http import HttpRequest, HttpResponseBadRequest
from django.urls.exceptions import NoReverseMatch
from django.utils.translation import gettext_lazy as _
from requests.exceptions import HTTPError
from rest_framework import serializers
from rest_framework.reverse import reverse

try:
    from allauth.account import app_settings as allauth_account_settings
    from allauth.socialaccount.helpers import complete_social_login
except ImportError:
    raise ImportError('allauth needs to be added to INSTALLED_APPS.')


class SocialLoginSerializer(serializers.Serializer):
    access_token = serializers.CharField(required=False, allow_blank=True)
    code = serializers.CharField(required=False, allow_blank=True)
    id_token = serializers.CharField(required=False, allow_blank=True)

    def _get_request(self):
        request = self.context.get('request')
        if not isinstance(request, HttpRequest):
            request = request._request
        return request

    def get_social_login(self, adapter, app, token, response):
        """
        :param adapter: allauth.socialaccount Adapter subclass.
            Usually OAuthAdapter or Auth2Adapter
        :param app: `allauth.socialaccount.SocialApp` instance
        :param token: `allauth.socialaccount.SocialToken` instance
        :param response: Provider's response for OAuth1. Not used in the
        :returns: A populated instance of the
            `allauth.socialaccount.SocialLoginView` instance
        """
        request = self._get_request()
        social_login = adapter.complete_login(request, app, token, response=response)
        social_login.token = token
        return social_login

    def set_callback_url(self, view, adapter_class):
        # first set url from view
        self.callback_url = getattr(view, 'callback_url', None)
        if not self.callback_url:
            # auto generate base on adapter and request
            try:
                self.callback_url = reverse(
                    viewname=adapter_class.provider_id + '_callback',
                    request=self._get_request(),
                )
            except NoReverseMatch:
                raise serializers.ValidationError(
                    _('Define callback_url in view'),
                )

    def validate(self, attrs):
        view = self.context.get('view')
        request = self._get_request()

        if not view:
            raise serializers.ValidationError(
                _('View is not defined, pass it as a context variable'),
            )

        adapter_class = getattr(view, 'adapter_class', None)
        if not adapter_class:
            raise serializers.ValidationError(_('Define adapter_class in view'))

        adapter = adapter_class(request)
        app = adapter.get_provider().app

        # More info on code vs access_token
        # http://stackoverflow.com/questions/8666316/facebook-oauth-2-0-code-and-token

        access_token = attrs.get('access_token')
        code = attrs.get('code')
        # Case 1: We received the access_token
        if access_token:
            tokens_to_parse = {'access_token': access_token}
            token = access_token
            # For # with apple
            id_token = attrs.get('id_token')
            if id_token:
                tokens_to_parse['id_token'] = id_token

        # Case 2: We received the authorization code
        elif code:
            self.set_callback_url(view=view, adapter_class=adapter_class)
            self.client_class = getattr(view, 'client_class', None)

            if not self.client_class:
                raise serializers.ValidationError(
                    _('Define client_class in view'),
                )

            # Removed Scope from here, as it is not used in the OAuth2 flow
            provider = adapter.get_provider()
            #scope = provider.get_scope_from_request(request)
            client = self.client_class(
                request,
                app.client_id,
                app.secret,
                adapter.access_token_method,
                adapter.access_token_url,
                self.callback_url,
                #scope,
                scope_delimiter=adapter.scope_delimiter,
                headers=adapter.headers,
                basic_auth=adapter.basic_auth,
            )
            try:
                token = client.get_access_token(code)
            except OAuth2Error as ex:
                print(ex)
                raise serializers.ValidationError(
                    _('Failed to exchange code for access token')
                ) from ex
            access_token = token['access_token']
            tokens_to_parse = {'access_token': access_token}

            # If available we add additional data to the dictionary
            for key in ['refresh_token', 'id_token', adapter.expires_in_key]:
                if key in token:
                    tokens_to_parse[key] = token[key]
        else:
            raise serializers.ValidationError(
                _('Incorrect input. access_token or code is required.'),
            )

        social_token = adapter.parse_token(tokens_to_parse)
        social_token.app = app

        try:
            if adapter.provider_id == 'google' and not code:
                login = self.get_social_login(adapter, app, social_token, response={'id_token': id_token})
            else:
                login = self.get_social_login(adapter, app, social_token, token)
            ret = complete_social_login(request, login)
        except HTTPError:
            raise serializers.ValidationError(_('Incorrect value'))

        if isinstance(ret, HttpResponseBadRequest):
            raise serializers.ValidationError(ret.content)

        if not login.is_existing:
            # We have an account already signed up in a different flow
            # with the same email address: raise an exception.
            # This needs to be handled in the frontend. We can not just
            # link up the accounts due to security constraints
            if allauth_account_settings.UNIQUE_EMAIL:
                # Do we have an account already with this email address?
                account_exists = get_user_model().objects.filter(
                    email=login.user.email,
                ).exists()
                if account_exists:
                    raise serializers.ValidationError(
                        _('User is already registered with this e-mail address.'),
                    )

            login.lookup()
            try:
                login.save(request, connect=True)
            except IntegrityError as ex:
                raise serializers.ValidationError(
                    _('User is already registered with this e-mail address.'),
                ) from ex
            self.post_#(login, attrs)

        attrs['user'] = login.account.user

        return attrs

    def post_#(self, login, attrs):
        """
        Inject behavior when the user signs up with a social account.

        :param login: The social login instance being registered.
        :type login: allauth.socialaccount.models.SocialLogin
        :param attrs: The attributes of the serializer.
        :type attrs: dict
        """
        pass

Step 2: Add the updated serializer to the GoogleLogin view.

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

class GoogleLogin(SocialLoginView): # if you want to use Authorization Code Grant, use this
    adapter_class = GoogleOAuth2Adapter
    callback_url = "https://developers.google.com/oauthplayground"
    client_class = OAuth2Client
    serializer_class = SocialLoginSerializer

Step 3: Make sure the following setting are updated.

REST_AUTH = {
    'USE_JWT': True,
    'JWT_AUTH_COOKIE': 'access',
    'JWT_AUTH_REFRESH_COOKIE': 'refresh'

}
SOCIALACCOUNT_PROVIDERS = {
    'google': {
        'SCOPE': [
            'profile',
            'email',
        ],
        'AUTH_PARAMS': {
            'access_type': 'offline',
        },
        'OAUTH_PKCE_ENABLED': True,
    }
} 

@toniengelhardt
Copy link

Thanks @joeychrys 🙏🏽

It seems like these two PRs (already merged) are resolving the problem: #655, #668.

@iMerica could we get a release for those?

@iamzoltan
Copy link

Facing the same issue. Any ideas when this will be integrated?

@kbrwl
Copy link

kbrwl commented Jan 4, 2025

Thanks @joeychrys 🙏🏽

It seems like these two PRs (already merged) are resolving the problem: #655, #668.

@iMerica could we get a release for those?

@iMerica requesting you to please put out a new release for these

@iMerica
Copy link
Owner

iMerica commented Jan 4, 2025

Published as 7.0.1

https://pypi.org/project/dj-rest-auth/7.0.1/

@iMerica iMerica closed this as completed Jan 4, 2025
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants