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

Support authorization code flow for Google OAuth #152

Open
samul-1 opened this issue Mar 26, 2023 · 6 comments
Open

Support authorization code flow for Google OAuth #152

samul-1 opened this issue Mar 26, 2023 · 6 comments
Labels
good first issue Good for newcomers

Comments

@samul-1
Copy link

samul-1 commented Mar 26, 2023

I have a requirement to store the access token and refresh token issued by Google when a user signs into my application in order to be able to perform requests to Google Classroom API on behalf of the user, after they've granted the relevant scopes to my app.

In order to do so, I would need the frontend application to send an authorization code, as opposed to a simple access token (which I cannot refresh on my backend) https://developers.google.com/identity/protocols/oauth2/web-server#exchange-authorization-code

However, I can see that the way the convert-token endpoint works, it requires a token parameter to be in the request.

Is there any way to support the authorization code flow, where the request simply contains an authorization code, I exchange it on my server for an access token + refresh token from Google, store them, then create the normal in-house access token and return it to the user? The last part is the same exact flow as the normal convert-token endpoint, but I first need to exchange the authorization code for access + refresh token.

Thank you in advance.

@wagnerdelima
Copy link
Owner

@samul-1 please be more concrete in your question. Explain how your pipeline works.
Your link is broken, it's just pointing to this repo's issues, please verify your link again.

@samul-1
Copy link
Author

samul-1 commented Apr 17, 2023

@samul-1 please be more concrete in your question. Explain how your pipeline works. Your link is broken, it's just pointing to this repo's issues, please verify your link again.

I fixed the link.

This issue has little to do with how my pipeline works.

Google OAuth2 authentication provides two flows:

  • client-side authentication (the one supported by this library), where the frontend is redirected to Google's consent page, completes the login, and obtains an access token, which is then sent to drf-social-oauth2, which verifies it and generates an in-house token
  • server-side authentication, or authorization code flow: in this case, the frontend completes the login and just obtains an authorization code from Google, which it then sends to the application backend. The application backend then exchanges that code for an access token and a refresh token. This allows the backend to store & use those tokens for making API calls to Google using the user's credentials I haven't been able to find a way to do this in drf-social-oauth2, despite the docstring in the ConvertTokenView says the authorization code flow is supported.

This is not a new request. Please see this PR made on the repo this project forked from: PR, and the issue mentioned here: issue.

I hope this is clearer now. I doubt this issue should've been closed to begin with, as I asked about the "authorization code flow", which is a standard term that this package claims to support, not something I made up in my comment.

@wagnerdelima wagnerdelima reopened this Apr 17, 2023
@wagnerdelima wagnerdelima added the good first issue Good for newcomers label Apr 17, 2023
@wagnerdelima
Copy link
Owner

I reopened the issue and I will work on this in the near future.

@walterbucolo
Copy link

Hey all, any progress on this?

@vied12
Copy link

vied12 commented Jun 20, 2024

I also needed to store the refresh_token on the backend, and manage to do it with this workaround

class ConvertTokenSerializer(Serializer):
    grant_type = CharField(max_length=50)
    backend = CharField(max_length=200)
    client_id = CharField(max_length=200)
    token = CharField(max_length=5000)
    refresh_token = CharField(max_length=5000)  # <----- we add the refresh token to the serializer inputs


class ConvertTokenView(BaseConvertTokenView):

    def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
        serializer = ConvertTokenSerializer(data=request.data) # <---- we use our custom serializer
        serializer.is_valid(raise_exception=True)
        # Use the rest framework `.data` to fake the post body of the django request.
        request._request.POST = request._request.POST.copy()  # type: ignore
        for key, value in serializer.validated_data.items():
            request._request.POST[key] = value  # type: ignore

        try:
            url, headers, body, status = self.create_token_response(request._request)
        except InvalidClientError:
            return Response(
                data={"invalid_client": "Missing client type."},
                status=HTTP_400_BAD_REQUEST,
            )
        except MissingClientIdError as ex:
            return Response(
                data={"invalid_request": ex.description},
                status=HTTP_400_BAD_REQUEST,
            )
        except InvalidRequestError as ex:
            return Response(
                data={"invalid_request": ex.description},
                status=HTTP_400_BAD_REQUEST,
            )
        except UnsupportedGrantTypeError:
            return Response(
                data={"unsupported_grant_type": "Missing grant type."},
                status=HTTP_400_BAD_REQUEST,
            )
        except AccessDeniedError:
            return Response(
                {"access_denied": "The token you provided is invalid or expired."},
                status=HTTP_400_BAD_REQUEST,
            )
        except IntegrityError as e:
            if "email" in str(e) and "already exists" in str(e):
                return Response(
                    {"error": "A user with this email already exists."},
                    status=HTTP_400_BAD_REQUEST,
                )
            else:
                return Response(
                    {"error": "Database error."},
                    status=HTTP_400_BAD_REQUEST,
                )
        except Exception as e:
            return Response(
                {"error": str(e)},
                status=HTTP_500_INTERNAL_SERVER_ERROR,
            )

        return Response(data=json_loads(body), status=status)
class ServiceOAuth2(OpenIdConnectAuth):
    name = "service"
    ...

    def user_data(self, access_token: str, *args: Any, **kwargs: Any) -> UserData:
        data: UserData = self.get_json(
            "https://service.net/core/connect/userinfo",
            headers={"Authorization": f"Bearer {access_token}"},
        )
        return {**data, "refresh_token": self.data.get("refresh_token")} # <---- we add the refresh token to the user data, so this is stored in DB

@wagnerdelima
Copy link
Owner

Guys, you can submit a pr here so that all of us can enhance the project from your experience and problems. I would be happy to contribute.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good first issue Good for newcomers
Projects
None yet
Development

No branches or pull requests

4 participants