django-allauth icon indicating copy to clipboard operation
django-allauth copied to clipboard

Google provider: first login using access token from an Android/iOS app works, but subsequent ones return code 401 (unauthorized) unless mobile app is uninstalled/reinstalled

Open sunweiyang opened this issue 1 year ago • 0 comments

I have a platform that consists of a React Native Android/iOS mobile app that uses React Native Google Sign In, a web app that uses react-google-login, and a Django server that uses django-allauth 0.45.0. We have created a REST endpoint based on dj-rest-auth and django-rest-framework to be processed using django-allauth's GoogleOAuth2Adapter.

For our Android/iOS app, the first attempt to log in works perfectly -- React Native Google Sign In communicates with Google to retrieve an access token, the access token gets sent to our server via our dj-rest-auth's Google LoginView, and the authentication is successfully processed via GoogleOAuth2Adapter.

However, it fails if the Android/iOS app user logs out and tries to log back in. React Native Google Sign In still successfully retrieves an access token, but the result is a 401 (unauthorized) response code by dj-rest-auth. This would happen for every Google login attempt after that first one.

Furthermore, when the Android/iOS app is uninstalled and reinstalled, the user can once again log in successfully -- but only once. Right now, we are directing our users to uninstall and reinstall the app whenever they need to log out and log back in via Google, but that is not ideal long-term.

It's also worth noting that our web app does not face this issue -- our web app users can use Google login to log in and out at will without problems.

My Google provider's configuration in my Django app using django-allauth:

SOCIALACCOUNT_PROVIDERS = {
    "google": {
        "SCOPE": ["profile", "email"],
        "AUTH_PARAMS": {
            "access_type": "offline",
        },
    }
}

My dj-rest-auth endpoint implementation:

from dj_rest_auth.views import LoginView
from rest_framework import serializers
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter

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)
    first_name = serializers.CharField(required=False, allow_blank=True)
    last_name = 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 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().get_app(request)

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

        # Case 1: We received the access_token
        if attrs.get("access_token"):
            access_token = attrs.get("access_token")

        # Case 2: We received the authorization code
        elif attrs.get("code"):
            self.callback_url = getattr(view, "callback_url", None)
            self.client_class = getattr(view, "client_class", None)

            if not self.callback_url:
                raise serializers.ValidationError(_("Define callback_url in view"))
            if not self.client_class:
                raise serializers.ValidationError(_("Define client_class in view"))

            code = attrs.get("code")

            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,
            )
            token = client.get_access_token(code)
            access_token = token["access_token"]

        else:
            raise serializers.ValidationError(
                _("Incorrect input. access_token or code is required.")
            )

        social_token = adapter.parse_token(
            {
                "access_token": access_token,
                "id_token": attrs.get("id_token"),  # For apple login
            }
        )
        social_token.app = app

        try:
            login = self.get_social_login(adapter, app, social_token, access_token)
            complete_social_login(request, login)
        except HTTPError:
            raise serializers.ValidationError(_("Incorrect value"))

        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_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(
                        _("Unable to log in with provided credentials.")
                    )

            login.lookup()
            login.save(request, connect=True)

        # Set user's first_name and/or last_name if provided by the client
        first_name = attrs.get("first_name")
        last_name = attrs.get("last_name")
        if first_name:
            login.account.user.first_name = first_name
        if last_name:
            login.account.user.last_name = last_name
        if first_name or last_name:
            login.account.user.save()

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

        return attrs

class GoogleLoginView(LoginView):
    serializer_class = SocialLoginSerializer
    adapter_class = GoogleOAuth2Adapter

My urls.py:

urlpatterns = [
    ...
    re_path(r"^api/rest-auth/google/$", GoogleLoginView.as_view(), name="google_login"),
    ...
]

sunweiyang avatar Sep 04 '22 00:09 sunweiyang