We have a SPA that will be served from a separate domain than the API backend. The SPA posts to an authenticate endpoint and receives a token.That view is setting the CSRF token for me as follows:
class Authenticate(APIView):
throttle_classes = ()
authentication_classes = ()
permission_classes = ()
serializer_class = AuthenticateSerializer
def post(self, request, *args, **kwargs):
google_id_token = request.data.get('google_id_token')
if not google_id_token:
return HttpResponseBadRequest('google_id_token is required')
# Authenticate w/ GoogleTokenBackend
user = authenticate(id_token=google_id_token)
if not user:
return HttpResponseForbidden('Unable to authenticate this google '
'account.')
user.csrf_token = get_token(request)
return Response(AuthenticateSerializer(user).data)
So now I have CSRF token coming back two ways from the server:
- The Set-Cookie header as expected.
- As part of my returned user object.
My first issue is that the AJAX request is not setting the cookie from the Set-Cookie request. Probably due to cross-domain stuff as the server is on :8000 and the SPA is on :3000. This is why I'm returning the value in the user response.
In my SPA once I receive that token I set it as an X-CSRFToken header (which is passed, confirmed by dev tools). However I receive the following error:
{"detail":"CSRF Failed: CSRF cookie not set."}
I checked the code of the CSRF middleware and see that it requires a cookie before falling back to the header.
if csrf_token is None:
# No CSRF cookie. For POST requests, we insist on a CSRF cookie,
# and in this way we can avoid all CSRF attacks, including login
# CSRF.
return self._reject(request, REASON_NO_CSRF_COOKIE)
# Check non-cookie token for match.
request_csrf_token = ""
if request.method == "POST":
try:
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
except IOError:
# Handle a broken connection before we've completed reading
# the POST data. process_view shouldn't raise any
# exceptions, so we'll ignore and serve the user a 403
# (assuming they're still listening, which they probably
# aren't because of the error).
pass
if request_csrf_token == "":
# Fall back to X-CSRFToken, to make things easier for AJAX,
# and possible for PUT/DELETE.
request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')
Is this intentional for security purposes? I don't really understand the point of having the header option if the cookie is still required.
Thanks!