Issues with CSRF on login form with React SPA and Django

I am building a React SPA with Django backend and Oauth using Django OAuth toolkit and have been asked by someone in the security team to implement CSRF protection on the login form of the app.

No CSRF cookie will be present as Django is not serving the frontend app nor will users be logging in via Django sessions.

I have created an API endpoint to provide CSRF tokens – to some degree following the instructions in this guide: https://www.stackhawk.com/blog/react-csrf-protection-guide-examples-and-how-to-enable-it/ The React app makes a call to this endpoint to retrieve a CSRF token.

To add the CSRF protection on the login url – provided by Oauth toolkit /o/token I have subclassed the Oauth toolkit TokenView and pointed my login URL at it. I have used a decorator to add csrf protection:

@method_decorator(csrf_protect, name="dispatch")
class GetToken(TokenView):
    pass

This seems to work correctly but when making the login request I cannot seem to get Django to accept the CSRF token I have retrieved from my endpoint – I am trying to provide it in the request body (as csrfmiddlewaretoken) and as a HTTP header (X-CSRFToken).

When I make the request and debug Django CSRF middleware I can see that the request is being rejected because POST requests require the token sent as a cookie only. Now I am considering whether to check the CSRF token manually in my subclassed view but I wonder if I am doing something fundamentally wrong here as my approach is feeling more and more hacky. Can anyone suggest a better method for adding the CSRF protection?

Thanks

Answer

Not sure but have you checked if the cookie is actually being set in the browser? Afaik you have to retrieve the cookie from a Django endpoint and then set it in your ajax call in the header of the post request.

You can make sure that the cookie is sent by your endpoint by adding the decorator “ensure_csrf_cookie” like this:

@method_decorator(ensure_csrf_cookie, name='dispatch')
class GetCSRFToken(APIView):
    permission_classes = (permissions.AllowAny,)
    def get(self, request, format=None):
        return Response({'success': 'CSRF returned'}, status=status.HTTP_200_OK)

Then retrieve the CSRF cookie with JS like this:

function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            // Does this cookie string begin with the name we want?
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}
const csrftoken = getCookie('csrftoken');

This is described in the docs: https://docs.djangoproject.com/en/3.2/ref/csrf/#ajax