dj-rest-auth: Google Social Authentication throws `Invalid id_token`
I’m using dj-rest-auth on the backend and NextAuth.js on the frontend. After successfully logging in via NextAuth.js I get the following account passed to my signIn callback:
{
provider: "google",
type: "oauth",
providerAccountId: "177780422328299215542",
access_token: "%access_token%",
expires_at: 1682337884,
refresh_token: "%refresh_token%",
scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid",
token_type: "Bearer",
id_token: "%id_token%"
}
access_tokenis a random string of characters. Not decodable by jwt.io.id_tokenis a JWT token (header.payload.signature). It is decodable by jwt.io.
I then forward the access_token and id_token to my dj-rest-auth Google endpoint defined like this:
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client, OAuth2Error
from dj_rest_auth.registration.views import SocialLoginView
class GoogleLogin(SocialLoginView): # Authorization Code grant
adapter_class = GoogleOAuth2Adapter
callback_url = "http://localhost:3000/api/auth/callback/google"
client_class = OAuth2Client
I use the following request code:
const options = {
method: 'POST',
url: 'http://127.0.0.1:8000/api/auth/google/',
headers: {
'Content-Type': 'application/json'
},
data: {
access_token: '%access_token%',
id_token: '%id_token%'
}
};
axios.request(options).then(function (response) {
console.log(response.data);
}).catch(function (error) {
console.error(error);
});
This request fails with an error saying:
Not enough segments
Internal Server Error: /api/auth/google/
Traceback (most recent call last):
File ".\venv\lib\site-packages\jwt\api_jws.py", line 251, in _load
header_segment, payload_segment = signing_input.split(b".", 1)
ValueError: not enough values to unpack (expected 2, got 1)
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File ".\authentication\views.py", line 33, in complete_login
identity_data = jwt.decode(
File ".\venv\lib\site-packages\jwt\api_jwt.py", line 168, in decode
decoded = self.decode_complete(
File ".\venv\lib\site-packages\jwt\api_jwt.py", line 120, in decode_complete
decoded = api_jws.decode_complete(
File ".\venv\lib\site-packages\jwt\api_jws.py", line 191, in decode_complete
payload, signing_input, header, signature = self._load(jwt)
File ".\venv\lib\site-packages\jwt\api_jws.py", line 253, in _load
raise DecodeError("Not enough segments") from err
jwt.exceptions.DecodeError: Not enough segments
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File ".\venv\lib\site-packages\django\core\handlers\exception.py", line 55, in inner
response = get_response(request)
File ".\venv\lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File ".\venv\lib\site-packages\django\views\decorators\csrf.py", line 56, in wrapper_view
return view_func(*args, **kwargs)
File ".\venv\lib\site-packages\django\views\generic\base.py", line 104, in view
return self.dispatch(request, *args, **kwargs)
File ".\venv\lib\site-packages\django\utils\decorators.py", line 46, in _wrapper
return bound_method(*args, **kwargs)
File ".\venv\lib\site-packages\django\views\decorators\debug.py", line 92, in sensitive_post_parameters_wrapper
return view(request, *args, **kwargs)
File ".\venv\lib\site-packages\dj_rest_auth\views.py", line 48, in dispatch
return super().dispatch(*args, **kwargs)
File ".\venv\lib\site-packages\rest_framework\views.py", line 509, in dispatch
response = self.handle_exception(exc)
File ".\venv\lib\site-packages\rest_framework\views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File ".\venv\lib\site-packages\rest_framework\views.py", line 480, in raise_uncaught_exception
raise exc
File ".\venv\lib\site-packages\rest_framework\views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File ".\venv\lib\site-packages\dj_rest_auth\views.py", line 125, in post
self.serializer.is_valid(raise_exception=True)
File ".\venv\lib\site-packages\rest_framework\serializers.py", line 227, in is_valid
self._validated_data = self.run_validation(self.initial_data)
File ".\venv\lib\site-packages\rest_framework\serializers.py", line 429, in run_validation
value = self.validate(value)
File ".\venv\lib\site-packages\dj_rest_auth\registration\serializers.py", line 151, in validate
login = self.get_social_login(adapter, app, social_token, response={'id_token': token})
File ".\venv\lib\site-packages\dj_rest_auth\registration\serializers.py", line 60, in get_social_login
social_login = adapter.complete_login(request, app, token, response=response)
File ".\authentication\views.py", line 46, in complete_login
raise OAuth2Error("Invalid id_token") from e
allauth.socialaccount.providers.oauth2.client.OAuth2Error: Invalid id_token
After some testing I figured out that response["id_token"] in complete_login returns the request’s access_token and not the id_token.
print(response["id_token"])
# prints out {'id_token': '%access_token%'}
Sending your id_token as the access_token without providing id_token seems to work:
const options = {
method: 'POST',
url: 'http://127.0.0.1:8000/api/auth/google/',
headers: {
'Content-Type': 'application/json'
},
data: {
access_token: '%id_token%'
}
};
axios.request(options).then(function (response) {
console.log(response.data);
}).catch(function (error) {
console.error(error);
});
Is this the expected behaviour? Am I missing something?
About this issue
- Original URL
- State: open
- Created a year ago
- Reactions: 2
- Comments: 31 (1 by maintainers)
I had the same problem, and I solved it by fixing the following dependency:
This solution works with dj-rest-auth==4.0.1
The root cause of this issue is that starting from 0.52.0, django-allauth is using the ID token for extracting user information. The ID token is handed over together with the access token as part of the Google OAuth handshake, so with just the django-allauth scope in mind there is no need to make additional calls to fetch user information. However, in a broader scope, this does break the dj-rest-auth use case, causing this issue.
In order to get this issue resolved, django-allauth has been changed to make a call to the userinfo endpoint in case no ID token is present. This change landed on version 0.61.0. However, dj-rest-auth is currently using this for its dependencies:
Given the amount of people impacted by this, I also backported that change to version 0.57.1 so that there is a version of dj-rest-auth that is compatible with a version of django-allauth containing the fix. This version has just been released. I do hope that current limitation of
<0.58.0gets resolved in a future release of dj-rest-auth, but at least on the short term we can now all move forward.Hope this helps!
Downgrading to 0.50.0 worked for me, hoping this is fixed soon in newer releases
I was working through @duplxey’s excellent tutorial article on Django REST Framework Authentication with Auth.js, and ran into this issue.
I wanted to note that duplexy has two similar example apps:
The new app uses:
Thanks to @pennersr the maintainer of django-allauth using a significant time to investigate this long term issue on our backend/app and push this to a solution that will hopefully benefit all users of django-allauth and dj-rest-auth. Forever grateful!
@ap-pjgr, it works if you pass both
access_tokenandid_token. Tested withdj-rest-auth==5.0.2anddjango-allauth==0.57.0.See this piece of code (
id_tokenis used only ifaccess_tokenis present): https://github.com/iMerica/dj-rest-auth/blob/23f097cebcc8ecef886b2ac7869cc1d51f66f90e/dj_rest_auth/registration/serializers.py#L100-L154If it helps anyone, I recently abandoned this whole login flow and instead started using the Google Identity API with one-tap sign-on. It’s a better user experience and is way easier to implement because it removes the complicated Oauth, redirect,
code, redirect flow that we’re using here. Instead, the user logs in with Google and you get a JWT with the user info right in the browser. You then post request this to your backend to create an account, fetch your own JWTs, etc.It feels like the way this “should” be done. The only downside I can see is that it removes Django allauth from the whole process, so if you need to support multiple social providers (Google, Facebook, etc.) it might make things more complicated. On the flip side, if you just need Google you can skip all the allauth overhead, extra tables, etc., and instead register users/retrieve details using your standard workflow.
More details here: https://developers.google.com/identity/gsi/web/guides/overview
This error only occurs with dj-rest-auth version 3.0.0. I fixed downgraded to2.2.8.
I can confirm that this is working, i use reactjs-social-login==2.6.2 to obtain the access_code from google
this is the response i get from reactjs-social-login
my backend looks like this, no config needed:
Hope it will help someone!
You’re the best! Thanks! This worked for me!
I had the same problem then I switched to
dj-rest-auth[with_social]==2.2.5and It just worked fine.I’ve been chasing this issue for the last few hours with the latest versions of Allauth (0.61.1) and dj-rest-auth (5.0.2) and by debugging both libraries, the underlying issue was being caused by the exception described here - https://github.com/jpadilla/pyjwt/issues/814, where when the OAuth2 token was attempted to be used within ms of being granted, it would fail.
As suggested in that thread, re-syncing my Windows clock actually fixed it. So throwing this here in case it’s helpful to anyone else in the future.
PS: Thanks again to pennersr for everything you do!
Confirmed to work on the following version combination by passing both
access_tokenandid_tokenin the POST to the backend Google views.Was this a bug before or is it a bug now? They are sparse, but any writeup I’ve read on this topic passes
id_tokenasaccess_tokenin the backend POST. That seemed strange to me when I read it initially. Perhaps the current behaviors not a bug but is rather the intended design. Can anyone confirm or clarify?Honestly, the whole id/access token is a bit opaque. Perhaps I needs to read some more in-depth discussions on this authentication architecture.
@DerekHill thanks for that, this allauth <–> dj-rest-auth gave me some awful headache
Sorry. My explanation is inadequate. refresh_token was meant to be a downgrade response to an error that returned with “”. Sorry for the confusion.
I’m very resistant to passing id_token to access_token, but it works anyway. Furthermore, in my case, I am also currently getting an error in the response where the refresh_token is returned as “”.