django-allauth
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
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"),
...
]