wagnerdelima/drf-social-oauth2

Support authorization code flow for Google OAuth

Opened this issue · 6 comments

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.

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

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

Hey all, any progress on this?

vied12 commented

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

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.