wagnerdelima / drf-social-oauth2

drf-social-oauth2 makes it easy to integrate Django social authentication with major OAuth2 providers, i.e., Facebook, Twitter, Google, etc.
https://drf-social-oauth2.readthedocs.io/en/latest/
MIT License
271 stars 34 forks source link

Support authorization code flow for Google OAuth #152

Open samul-1 opened 1 year ago

samul-1 commented 1 year ago

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

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

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

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

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

walterbucolo commented 7 months ago

Hey all, any progress on this?

vied12 commented 2 months ago

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

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.