iMerica / dj-rest-auth

Authentication for Django Rest Framework
https://dj-rest-auth.readthedocs.io/en/latest/index.html
MIT License
1.66k stars 310 forks source link

Google Social Authentication throws `Invalid id_token` #503

Open duplxey opened 1 year ago

duplxey commented 1 year ago

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?

cplanck commented 1 year ago

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.

rizwanriaz-se commented 1 year ago

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

On Mon, 24 Apr 2023, 16:54 Nik Tomazic, @.***> wrote:

I'm using dj-rest-auth on the backend and NextAuth.js https://next-auth.js.org/ 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 GoogleOAuth2Adapterfrom allauth.socialaccount.providers.oauth2.client import OAuth2Client, OAuth2Errorfrom 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?

— Reply to this email directly, view it on GitHub https://github.com/iMerica/dj-rest-auth/issues/503, or unsubscribe https://github.com/notifications/unsubscribe-auth/ASMTYPFSD6SPWZ4HZRZX2Y3XCZSXPANCNFSM6AAAAAAXJO35P4 . You are receiving this because you are subscribed to this thread.Message ID: @.***>

agent-Y commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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

duplxey commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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

rizwanriaz-se commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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

BarnabasSzabolcs commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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

ap-pjgr commented 12 months ago

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

0x29a commented 10 months ago

@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): https://github.com/iMerica/dj-rest-auth/blob/23f097cebcc8ecef886b2ac7869cc1d51f66f90e/dj_rest_auth/registration/serializers.py#L100-L154

RonAlmog commented 9 months ago

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 commented 9 months ago

django-allauth==0.50.0

Thank you so much ❤️

monoprosito commented 9 months ago

@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:

https://github.com/iMerica/dj-rest-auth/blob/23f097cebcc8ecef886b2ac7869cc1d51f66f90e/dj_rest_auth/registration/serializers.py#L100-L154

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 commented 9 months ago

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 commented 8 months ago

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 commented 8 months ago

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 commented 8 months ago

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 commented 7 months ago

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 commented 6 months ago

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!

marty0678 commented 6 months ago

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 commented 5 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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

samirul commented 2 months ago

Hello, How to solve "Failed to exchange code for access token" When sending {"code": "code from google"} on postman ?