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

Google Social Authentication throws Invalid id_token #503

Open
duplxey opened this issue Apr 24, 2023 · 35 comments
Open

Google Social Authentication throws Invalid id_token #503

duplxey opened this issue Apr 24, 2023 · 35 comments

Comments

@duplxey
Copy link

duplxey commented Apr 24, 2023

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%"
}
  1. access_token is a random string of characters. Not decodable by jwt.io.
  2. id_token is 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?

@duplxey duplxey changed the title Google Social Authentication throws allauth.socialaccount.providers.oauth2.client.OAuth2Error: Invalid id_token Google Social Authentication throws Invalid id_token Apr 24, 2023
@cplanck
Copy link

cplanck commented Apr 26, 2023

I had this same problem and came to the similar conclusion. In my case I overrode GoogleOAuth2Adapter and changed response["id_token"] to response["id_token"]["id_token"] which fixed it.

@iamrizwan077
Copy link

iamrizwan077 commented Apr 27, 2023 via email

@agent-Y
Copy link

agent-Y commented Apr 28, 2023

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 "".

@cplanck
Copy link

cplanck commented Apr 28, 2023

@agent-Y do you have JWT_AUTH_HTTPONLY=False defined in your settings? Per the docs, if JWT_AUTH_HTTPONLY=True (default) the refresh token won't be sent. Initially I wanted to keep it true and just shuttle the JWT cookie back and fourth, but I wasn't able to get that working so I moved to sending the tokens in the body and saving them in local storage.

@agent-Y
Copy link

agent-Y commented Apr 29, 2023

@cplanck Actually I'm using dj-rest-auth with djangorestframework-simplejwt (https://django-rest-framework-simplejwt.readthedocs.io/en/latest/getting_started.html# ), so I guess it is not dj-rest-auth's fault.
But I'm looking for someone who is in the same situation.

@agent-Y
Copy link

agent-Y commented Apr 29, 2023

This error only occurs with dj-rest-auth version 3.0.0. I fixed downgraded to2.2.8.

@duplxey
Copy link
Author

duplxey commented May 2, 2023

This error only occurs with dj-rest-auth version 3.0.0. I fixed downgraded to2.2.8.

I suppose this response is referring to the JWT problem, correct @agent-Y? I tried it with 2.2.8 and I still get Invalid id_token if I don't pass my id_token as the access_token.

@duplxey
Copy link
Author

duplxey commented May 2, 2023

Yeah, I only passed access_token to dj_rest_auth endpoint and it worked, as there was nothing as id_token returned from the Google client library..

Interesting @iamrizwan077, I tried passing the id_token and access_token from the official Google OAuth 2.0 Playground and it didn't seem to fix anything. They were both included in the response though.

@cplanck
Copy link

cplanck commented May 2, 2023

If 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

@iamrizwan077
Copy link

iamrizwan077 commented May 3, 2023

Yeah, I only passed access_token to dj_rest_auth endpoint and it worked, as there was nothing as id_token returned from the Google client library..

Interesting @iamrizwan077, I tried passing the id_token and access_token from the official Google OAuth 2.0 Playground and it didn't seem to fix anything. They were both included in the response though.

@duplxey Here is the relevant frontend code that I used:

import { GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google';
const Login = () => {
    const responseGoogle = (response) => {
        // Send the response (code or token) to your Django backend to authenticate the user
        axios('https://iamrizwan066.pythonanywhere.com/dj-rest-auth/google/', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            data: JSON.stringify({
                access_token: response.credential,
            }),
        })
            .then((res) => {
                if (res.status === 200) {
                    navigate('/')
                }
                //Sending credential(Access token) part to django for token
                responseToken(jwt_decode(response.credential));
            })
    }

    return (
        <div>
            ...
            {/* Google Sign In button */}
            <GoogleOAuthProvider clientId={`${googleClientId}`}>
                <GoogleLogin
                    onSuccess={(credentialResponse) => {
                        responseGoogle(credentialResponse)
                    }}
                    onError={() => {
                        console.log('Login Failed');
                        toast("Login failed!")
                    }}
                />
            </GoogleOAuthProvider>
            ...
        </div>
    );
}

The given Google Client library returns me a credential (jwt access token), client_id and select_by attribute. I send that credential to dj-rest-auth endpoint with key 'access_token' and it works. If I send any other key, I get error in console 'access_token expected'..
And have this in my backend:

class GoogleLogin(SocialLoginView):
    adapter_class = GoogleOAuth2Adapter
    callback_url = 'https://picspacevault.netlify.app' //my website frontend url
    client_class = OAuth2Client

Since I am using Django Tokens for auth, so I am sending a further request to my custom URL for retrieving Django token from database based on user data sent through that credential..

@agent-Y
Copy link

agent-Y commented May 3, 2023

This error only occurs with dj-rest-auth version 3.0.0. I fixed downgraded to2.2.8.

I suppose this response is referring to the JWT problem, correct @agent-Y? I tried it with 2.2.8 and I still get Invalid id_token if I don't pass my id_token as the access_token.

Sorry. My explanation is inadequate.
refresh_token was meant to be a downgrade response to an error that returned with "".
Sorry for the confusion.

@umair313
Copy link

I had the same problem then I switched to dj-rest-auth[with_social]==2.2.5 and It just worked fine.

@BarnabasSzabolcs
Copy link

BarnabasSzabolcs commented May 29, 2023

I had the same problem,
and I solved it by fixing the following dependency:

django-allauth==0.50.0

This solution works with dj-rest-auth==4.0.1

@adrenaline681
Copy link
Contributor

I had the same problem, and I solved it by fixing the following dependency:

django-allauth==0.50.0

This solution works with dj-rest-auth==4.0.1

Downgrading to 0.50.0 worked for me, hoping this is fixed soon in newer releases

@matiasvallejosdev
Copy link

matiasvallejosdev commented Jul 4, 2023

I had the same problem, and I solved it by fixing the following dependency:

django-allauth==0.50.0

This solution works with dj-rest-auth==4.0.1

You're the best! Thanks! This worked for me!

@cwazuidema
Copy link

I can confirm that this is working, i use reactjs-social-login==2.6.2 to obtain the access_code from google

django-allauth==0.50.0
dj-rest-auth==4.0.1

this is the response i get from reactjs-social-login

{
    "access_token": "access_token",
    "token_type": "Bearer",
    "expires_in": 3599,
    "scope": "email profile openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile",
    "authuser": "0",
    "prompt": "none",
    "sub": "sub",
    "name": "Me",
    "given_name": "Me",
    "family_name": "And I",
    "picture": "url",
    "email": "email@gmail.com",
    "email_verified": true,
    "locale": "en"
}

my backend looks like this, no config needed:

from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter

class GoogleLogin(SocialLoginView):
    adapter_class = GoogleOAuth2Adapter

Hope it will help someone!

@mazzara
Copy link

mazzara commented Aug 8, 2023

More information on the issue

As of Aug, 08, 2023, with django-allauth latest version 0.54.0 it seems to have a bug that results in function being unable to pass user identity to other funcitons, resulting in a frustrating code breake. If you face this error it can be worked around by a downgrade, pip install django-allauth==0.50.0 to resolve an issue with allauth library that results in error The above exception (not enough values to unpack (expected 2, got 1)). Issue in hand is with a response["id_token"] which in upgraded version the new function seems to have change unpack parameters, but older version 0.50.0 works fine.
This is an annoying and poorly documented error, so notes came handy to work arround until bug is fixed:

Environment Information for reproducing this error:
Django version: 3.2.18
Python version 3.11.4
django-allauth 0.54.0
google-auth 2.21.0
google-auth-httplib2 0.1.0
google-auth-oauthlib 1.0.0
googleapis-common-protos 1.59.1
djangorestframework 3.14.0
djangorestframework-simplejwt 5.2.2
dj-rest-auth 4.0.1

Error reproduction

Trying to hook oauth using a simple general GoogleLogin function to call Google Oauth api with library resources returns an error response suggesting an invalid token id.

class GoogleLogin(SocialLoginView):
    adapter_class = GoogleOAuth2Adapter

Calling api http://127.0.0.1:8000/api/rest-auth/google/ with provided credentials generated at Google Playground will result in an Invalid Token error.

{
  "access_token": <access_token>,
  "id_token": <id_token>,
  "expires_in": 3599, 
  "token_type": "Bearer", 
  "scope": "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid", 
  "refresh_token": <refresh_token>
}

The above exception (not enough values to unpack (expected 2, got 1)) was the direct cause of the following exception: /opt/venv//lib/python3.11/site-packages/allauth/socialaccount/providers/google/views.py, line 42, in complete_login*

class GoogleOAuth2Adapter(OAuth2Adapter):
    provider_id = GoogleProvider.id
    access_token_url = ACCESS_TOKEN_URL
    authorize_url = AUTHORIZE_URL
    id_token_issuer = ID_TOKEN_ISSUER

    def complete_login(self, request, app, token, response, **kwargs):
        try:
            identity_data = jwt.decode(
                response["id_token"],
                # Since the token was received by direct communication
                # protected by TLS between this library and Google, we
                # are allowed to skip checking the token signature
                # according to the OpenID Connect Core 1.0
                # specification.
                # https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
                options={
                    "verify_signature": False,
                    "verify_iss": True,
                    "verify_aud": True,
                    "verify_exp": True,
                },
                issuer=self.id_token_issuer,
                audience=app.client_id,
            )
        except jwt.PyJWTError as e:
            raise OAuth2Error("Invalid id_token") from e
        login = self.get_provider().sociallogin_from_response(request, identity_data)
        return login

Class handeling this in version 0.50.0 seems to work fine:

class GoogleOAuth2Adapter(OAuth2Adapter):
    provider_id = GoogleProvider.id
    access_token_url = "https://accounts.google.com/o/oauth2/token"
    authorize_url = "https://accounts.google.com/o/oauth2/auth"
    profile_url = "https://www.googleapis.com/oauth2/v1/userinfo"

    def complete_login(self, request, app, token, **kwargs):
        resp = requests.get(
            self.profile_url,
            params={"access_token": token.token, "alt": "json"},
        )
        resp.raise_for_status()
        extra_data = resp.json()
        login = self.get_provider().sociallogin_from_response(request, extra_data)
        return login

The changes between the versions seem to reflect a transition from a simple OAuth 2.0 flow (where user data is fetched from the provider) to an OpenID Connect flow (where user identity information is embedded in the id_token). The OpenID Connect is an extension of OAuth 2.0 and provides richer identity features.

However, as with any update, there might be some changes in the library's internal handling that could lead to the issue. As there seems to be a bug or unexpected behavior in the newer version that resulted in the "not enough values to unpack" error, downgrading to the older version that doesn't have this issue is a reasonable short-term fix. Always good to monitor the library for any patches or updates that address the issue in future versions.

I hope this detailed report helps others facing similar issues and aids in the quicker resolution of this bug. Thank you to the community and maintainers for their continuous efforts in maintaining and improving this library.

@DerekHill
Copy link

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:

  1. https://github.com/duplxey/django-rest-allauth – This is older, and I believe has this same issue (I first found it via Reddit)
  2. https://github.com/duplxey/django-rest-authjs – This is newer, and works great.

The new app uses:

django-allauth==0.54.0
dj-rest-auth==4.0.1

@denyswsu
Copy link

denyswsu commented Oct 5, 2023

@DerekHill thanks for that, this allauth <--> dj-rest-auth gave me some awful headache

@ap-pjgr
Copy link

ap-pjgr commented Oct 9, 2023

While trying to implement the previous guide (https://testdriven.io/blog/django-rest-authjs/), my team also faced some of the issues described above:

  • versions mismatch between django-allauth and dj-rest-auth
    In the end, using version 0.54.0 (allauth) and 4.0.1 (dj-rest-auth) as suggested above seemed to work but these are not the latest versions of these packages which we would like to use

  • passing the id_token in the access_token field was the only way we managed to properly authenticate with the backend. Is this intended? Will it change in the future?

@0x29a
Copy link

0x29a commented Dec 2, 2023

@ap-pjgr, it works if you pass both access_token and id_token. Tested with dj-rest-auth==5.0.2 and django-allauth==0.57.0.

See this piece of code (id_token is used only if access_token is present):

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'),
)
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,
)
try:
token = client.get_access_token(code)
except OAuth2Error as 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

@RonAlmog
Copy link

Thanks @DerekHill for the tip. I took the latest version of @duplxey, and only with these specific versions the thing worked, and now i'm able to login with google authorization. btw it works also with the latest django, not only 4.23 as mentioned in his requirements.txt .
The thing is, the whole thing is a bit fragile. so many libraries are involved, and it only works in very specific versions... and there's lots of black magic behind it. I'm a bit worried to use this in production.

@HaseebImd
Copy link

django-allauth==0.50.0

Thank you so much ❤️

@monoprosito
Copy link

@ap-pjgr, it works if you pass both access_token and id_token. Tested with dj-rest-auth==5.0.2 and django-allauth==0.57.0.

See this piece of code:

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'),
)
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,
)
try:
token = client.get_access_token(code)
except OAuth2Error as 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

Yes, you can use the Google OAuth 2.0 Playground to get your ID Token and Access Token. This post can help you: https://duizendstra.medium.com/how-to-easily-obtain-a-google-id-token-f1cde61541f0

@sherrellbc
Copy link

Confirmed to work on the following version combination by passing both access_token and id_token in the POST to the backend Google views.

dj-rest-auth==5.0.2
Django==4.2.2
django-allauth==0.57.0

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_token as access_token in 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.

@rostgoat
Copy link

rostgoat commented Feb 6, 2024

django-allauth==0.50.0

Yea this worked for me. Specifically, the following:

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
    callback_url = "http://127.0.0.1:3000/"
    client_class = OAuth2Client

    def post(self, request, *args, **kwargs):
        print(f"Request data: {request.data}")

        request.data["id_token"] = request.data.get("access_token")
        return super().post(request, *args, **kwargs)

Seems like a bug.

@pennersr
Copy link

pennersr commented Feb 7, 2024

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:

    extras_require={
        'with_social': ['django-allauth>=0.56.0,<0.58.0'],
    },

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.0 gets 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!

@henningbra
Copy link

henningbra commented Feb 7, 2024

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!

@sumantagogoi
Copy link

sumantagogoi commented Feb 11, 2024

I was getting an issue with this too. What was happening was: when the frontend sends the access_token to the GoogleLogin endpoin as "token"
earlier version of dj-rest-auth (2.2.4, 2.2.6) the response would be a "refresh_token", "access_token" and "user" object.

newer dj-rest-auth versions was causing the response to be just {"key":"xxxxxxxxxxxxxxxxxxx"}.

my frontend was expecting the older format "access_token" "refresh_token" and user object. not "key". I didnt even know what to do with the key value.

So I downgraded to allauth 0.50 and dj-rest-auth 2.2.6 and it is working fine again.

@marty0678
Copy link

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 - jpadilla/pyjwt#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!

@marty0678
Copy link

So update to the above. Even with re-syncing my Windows clock every few hours, this is still pretty flakey and is still failing about 50% of the time. It seems even a delta of a few MS will throw this off.

So this is what I came up with, and it seems to be much more reliable (even manually setting my local time back by a minute and increasing the allowed delta_time window to 90 to accommodate it, the request, while taking over a minute, doesn't fail).

class GoogleOAuth2IatValidationAdapter(GoogleOAuth2Adapter):
    def complete_login(self, request, app, token, response, **kwargs):
        try:
            delta_time = (
                jwt.decode(
                    response.get("id_token"),
                    options={"verify_signature": False},
                    algorithms=["RS256"],
                )["iat"]
                - time()
            )
        except jwt.PyJWTError as e:
            raise OAuth2Error("Invalid id_token during 'iat' validation") from e
        except KeyError as e:
            raise OAuth2Error("Failed to get 'iat' from id_token") from e

		# Or change 30 to whatever you feel is a maximum amount of time you are willing to wait
        if delta_time > 0 and delta_time <= 30:
            sleep(delta_time)

        return super().complete_login(request, app, token, response, **kwargs)


class GoogleLoginView(SocialLoginView):
    adapter_class = GoogleOAuth2IatValidationAdapter
    ...

The JWT is still fully validated as part of the super().complete_login so this shouldn't compromise the JWT AFAIK, but having a sleep call in a request isn't my favourite. If anyone has an alternative solution/suggestion lmk but I'll see how reliable this is over the next few days.

@openSourceBugs
Copy link

What exactly is the resolution for this? I am also getting the ""Failed to exchange code for access token" error and I don't know why. There at least has to be better debug output for this, and it should be fixed.

@TranDatk
Copy link

More information on the issue

As of Aug, 08, 2023, with django-allauth latest version 0.54.0 it seems to have a bug that results in function being unable to pass user identity to other funcitons, resulting in a frustrating code breake. If you face this error it can be worked around by a downgrade, pip install django-allauth==0.50.0 to resolve an issue with allauth library that results in error The above exception (not enough values to unpack (expected 2, got 1)). Issue in hand is with a response["id_token"] which in upgraded version the new function seems to have change unpack parameters, but older version 0.50.0 works fine.

It worked for me, thank you very much.

@Kaveks
Copy link

Kaveks commented May 17, 2024

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%"
}
1. `access_token` is a random string of characters. Not decodable by jwt.io.

2. `id_token` is 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?

@Kaveks
Copy link

Kaveks commented May 17, 2024

The problem is with django-allauth versions ,the newer versions seems to have a bug arround id_token, I downgraded django-allauth==0.62.1 to django-allauth==0.57.0 and this worked with django-rest-auth==6.0.0

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