Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

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

Open
shennnj opened this issue May 31, 2024 · 12 comments
Open

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 sign in 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_signup(login, attrs)

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

        return attrs

    def post_signup(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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants