djoser: Problem using social auth in stateless webapp

I have the same problem as here, meaning, the last step of Google OAuth2 authentication is not working. After some searching, I saw that the problem comes from the validation of state : the value in the request is checked against the value from the previous request that was saved in session. My problem, and I suppose it is the same one as @Emnalyeriar’s, is that my app is stateless, I don’t use session nor cookies so getting previous value of state is impossible, nor is it restful. djoser main target are stateless apps, not being able to use the OAuth2 protocol (which is the standard for most providers) make social auth unusable. Any use of session should therefore be removed. What do you think ?

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 10
  • Comments: 19 (2 by maintainers)

Most upvoted comments

If anyone’s interested I created my own social login views with requests_oauthlib:

class GoogleOAuth2(APIView):
    """
    Login with Google OAuth2
    """

    def get(self, request):
        client_id = settings.GOOGLE_OAUTH2_KEY
        scope = settings.GOOGLE_OAUTH2_SCOPE
        redirect_uri = request.query_params.get('redirect_uri')
        if redirect_uri not in settings.SOCIAL_AUTH_ALLOWED_REDIRECT_URIS:
            return response.Response(
                {
                    'error': 'Wrong Redirect URI'
                },
                status=status.HTTP_400_BAD_REQUEST,
            )
        google = OAuth2Session(client_id, scope=scope, redirect_uri=redirect_uri)
        authorization_url, state = google.authorization_url(
            settings.GOOGLE_AUTHORIZATION_BASE_URL,
            access_type='offline',
            prompt='select_account'
        )
        return response.Response({'authorization_url': authorization_url})

    def post(self, request):

        client_id = settings.GOOGLE_OAUTH2_KEY
        client_secret = settings.GOOGLE_OAUTH2_SECRET

        state = request.data.get('state')
        code = request.data.get('code')
        redirect_uri = request.data.get('redirect_uri')

        google = OAuth2Session(
            client_id,
            redirect_uri=redirect_uri,
            state=state
        )
        google.fetch_token(
            settings.GOOGLE_TOKEN_URL,
            client_secret=client_secret,
            code=code
        )

        user_info = google.get('https://www.googleapis.com/oauth2/v1/userinfo').json()
        user_email = user_info['email']
        try:
            user = User.objects.get(email=user_email)
        except User.DoesNotExist:
            # Decide if you want to create a new user
            user = User.objects.create_user()
        refresh_token = RefreshToken.for_user(user)
        return response.Response({
            'refresh': str(refresh_token),
            'access': str(refresh_token.access_token)
        })

I also have one for FB but its very similar

Damn, that’s a shame. I was hoping to switch from django-rest-auth to djoser for a complete restful local account and social solution. I’m currently trying to get django-rest-social-auth working for the social side of things but it sucks having to use multiple different libraries.

Any update on this?

I think the code above by @Emnalyeriar is not really secure: It basically does not check if the state in the POST matches the state generated in the GET:

  • state in GET is stored as variable, but never used again, the OAuth session is discarded after the request - which is not the case in the requests_oauthlib examples
  • in the POST a new OAuth session is initialized simply with the state passed in the request - ignoring the state value that was generated in the GET

As I understand this allows CSRF attacks, the implementation should check if both states are the same. See for example the Github OAuth docs on state:

Bildschirmfoto 2020-05-03 um 15 59 26

I’m not sure what a correct solution here is. Obviously sessions work which is what the Django social auth lib uses. Maybe it is sufficient to set the state as a cookie in the GET response which can be used to initialize state in the OAuth session in the POST - but I’m far from an expert in this matter.

Edit: After looking some more into the matter, the cookie approach will only work if you use same-site cookies. That in turn will only work if your frontend is hosted on the same domain as your REST API.

If your frontend is hosted on another domain, it should be a valid approach to:

  • issue a signed CSRF token from your REST API in the GET, the token containing the state value
  • store the token in the browser in the frontend domain (local storage or cookie)
  • pass the CSRF token in the POST request to the API, decode and verify it’s signature to make sure it was issued by your API
  • use the decoded state to initialize the OAuth session and pass in the complete authorization callback URL including code and state to make OAuth session verify that the states match. Alternatively compare states manually
  • the token should also contain a timestamp that is validated at this point to protect against replay attacks: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#encryption-based-token-pattern

I would add session auth as own auth method. And, try to separate further, ie not use sessions at all in other auth methods, if possible? a test suite that disables sessions for some cases would ensure this stays that way.

In fact, django-rest-social-auth uses social-django as well, they just basically negate all of the session stuff in a custom strategy