2

I'm trying to build a Single Page Application with Django Rest Framework. For authentication, I'm using a login view that initiates a session and requires csrf protection on all api routes. Because there is no templating going on, the csrf_token tag is never used, so I have to manually get the token with get_token. Instead of putting it in the main index file that will be given in the home view, I want to set it on its own cookie. No this is not the CSRF Cookie that django provides, as that one has the CSRF secret, plus I mentioned I'm using sessions, so the secret is stored there. This cookie will have the token which will be used for all mutating requests.

I have tried everything to get django to accept the cookie but nothing has worked. I can login just fine the first time, because there was no previous session, but anything after that just throws an error 403. I tried using ensure_csrf_cookie but that didn't help. I tried without sessions and still nothing. I even tried rearranging the middleware order and still nothing. I even tried my own custom middleware to create the cookie but it didn't work. Here's the code that I have ended up with as of now:

views.py

@api_view(http_method_names = ["GET"])
def home(request):
    """API route for retrieving the main page of web application"""
    return Response(None, status = status.HTTP_204_NO_CONTENT);

class LoginView(APIView):
    """API endpoint that allows users to login"""
    def post(self, request, format = None):
        """API login handler"""
        user = authenticate(username = request.data["username"], password = request.data['password']);
        if user is None:
            raise AuthenticationFailed;
        login(request, user);
        return Response(UserSerializer(user).data);

class LogoutView(APIView):
    """API endpoint that allows users to logout of application"""
    def post(self, request, format = None):
        logout(request);
        return Response(None, status = status.HTTP_204_NO_CONTENT);

settings.py

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware'
]

# Sessions

SESSION_ENGINE = "django.contrib.sessions.backends.file"
SESSION_FILE_PATH = os.getenv("DJANGO_SESSION_FILE_PATH")
SESSION_COOKIE_AGE = int(os.getenv("SESSION_LIFETIME")) * 60
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_NAME = os.getenv("SESSION_COOKIE")
SESSION_COOKIE_SECURE = os.getenv("APP_ENV") != "local"

# CSRF
CSRF_USE_SESSIONS = True
# the following setting is for the csrf token only, not for the CSRF secret, which is the default for django
CSRF_TOKEN_CARRIER = os.getenv("XSRF_COOKIE")
CSRF_HEADER_NAME = "X-XSRF-TOKEN"
CSRF_COOKIE_SECURE = SESSION_COOKIE_SECURE
CSRF_COOKIE_AGE = SESSION_COOKIE_AGE
CSRF_TOKEN_HTTPONLY = False

REST_FRAMEWORK = {
    "EXCEPTION_HANDLER":"django_app.application.exceptions.global_exception_handler",
    "DEFAULT_AUTHENTICATION_CLASSES":[
        "rest_framework.authentication.SessionAuthentication"
    ]
}

this was the custom middleware I wrote but I'm not using right now:

"""Custom CSRF Middleware for generating CSRF cookies"""
from django.conf import settings;
from django.middleware.csrf import CsrfViewMiddleware, rotate_token, get_token;

class CSRFCookieMiddleware:
    """Sets CSRF token cookie for ajax requests"""
    def __init__(self, get_response):
        self.get_response = get_response;

    def __call__(self, request):
        response = self.get_response(request);
        if settings.CSRF_USE_SESSIONS:
            response.set_cookie(
                settings.CSRF_TOKEN_CARRIER,
                get_token(request),
                max_age=settings.CSRF_COOKIE_AGE,
                domain=settings.CSRF_COOKIE_DOMAIN,
                path=settings.CSRF_COOKIE_PATH,
                secure=settings.CSRF_COOKIE_SECURE,
                httponly=settings.CSRF_COOKIE_HTTPONLY,
                samesite=settings.CSRF_COOKIE_SAMESITE,
            );
        return response;

the error 403 response:

HTTP/1.1 403 Forbidden
Date: Thu, 25 Apr 2019 17:11:28 GMT
Server: WSGIServer/0.2 CPython/3.7.0
Content-Type: application/json
Vary: Accept, Cookie
Allow: POST, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 59

{
  "message": "CSRF Failed: CSRF token missing or incorrect."
}

this is the http request I use in my REST client in vs code:

POST http://electro:8000/api/logout
X-XSRF-TOKEN: JFaygAm49v6wChT6CcUJaeLwq53YwzAlnEZmoE0m21cg9xLCnZGvTt6oM9MKbvV8
Cookie: electro=nzzv64gymt1aqu4whdhax1s9t91c3m58


I cannot believe how hard it is to tweak frameworks to work with single page apps when there's plenty support for static websites and APIs. So where have I gone wrong?

OzzyTheGiant
  • 711
  • 8
  • 21
  • Are you sure that you are [sending token from your client-side correctly](https://stackoverflow.com/q/34558264/10310124)? – ar-m-an Apr 25 '19 at 20:02
  • @ar-m-an see my edited question. I added the http request that I send. Instead of the default `X-CSRFToken` header, I use `X-XSRF-TOKEN`, according to settings.py module. I also send the session cookie back up to server. I have also tried setting the XSRF_TOKEN cookie back with the token value but that didn't work either. As for the answer you provided, I'm not using any javascript at the moment, this is all purely tested on a REST client in vs code. – OzzyTheGiant Apr 25 '19 at 20:10

2 Answers2

6

I finally figured out what happened. Buried deep in the django documentation, I found out that the CSRF_HEADER_NAME setting has a specific syntax/format:

# default value
CSRF_HEADER_NAME = "HTTP_X_CSRFTOKEN";

so to fix this, the docs literally say that for my case I must set the value, according to my preferences, like so:

CSRF_HEADER_NAME = "HTTP_X_XSRF_TOKEN";

So now it can accept the token at X-XSRF-TOKEN header, along with session cookie. But since I'm using sessions with csrf, I must use the custom middleware I created (see question) to set the csrf token cookie manually. This is because ensure_csrf_cookie apparently only throws you the session cookie.

Lastly, if you need to protect the login route, since I'm using SessionAuthentication, I will need the custom middleware, ensure_csrf_cookie, and csrf_protect so that I can get a starting session with csrf token and then submit those when logging in:

@api_view(http_method_names = ["GET"])
@ensure_csrf_cookie
def home(request):
    """API route for retrieving the main page of web application"""
    return Response(None, status = status.HTTP_204_NO_CONTENT);

@method_decorator(csrf_protect, 'dispatch')
@method_decorator(ensure_csrf_cookie, 'dispatch')
class LoginView(APIView):
    """API endpoint that allows users to login"""
    def post(self, request, format = None):
        """API login handler"""
        user = authenticate(username = request.data["username"], password = request.data['password']);
        if user is None:
            raise AuthenticationFailed;
        login(request, user);
        return Response(UserSerializer(user).data);

may this help whoever is building a Single Page App backend with django

OzzyTheGiant
  • 711
  • 8
  • 21
0

The documentation seems to be misleading regarding the replacement of hyphen to underscores.

It states:

Default: 'HTTP_X_CSRFTOKEN'

The name of the request header used for CSRF authentication.

As with other HTTP headers in request.META, the header name received from the server is normalized by converting all characters to uppercase, replacing any hyphens with underscores, and adding an 'HTTP_' prefix to the name. For example, if your client sends a 'X-XSRF-TOKEN' header, the setting should be 'HTTP_X_XSRF_TOKEN'.

I was using fetch, which lowercases all header names.
I named my client side header 'X_CSRFTOKEN', which got sent out as 'x_csrftoken'.
The request did not work until I changed the name to 'x-csrftoken'.

I found a line in Django that converts underscores to hyphens, which may be the problem.

tl;dr: To match Django's default HTTP_X_CSRFTOKEN, name your client header x-csrftoken.

Mero
  • 26
  • 5