Forbidden (CSRF cookie not set.) when sending POST/DELETE request from Vue.js to Django
Asked Answered
L

1

6

I have been trying for a while to send a POST or DELETE request from my Vue front-end to my Django backend.

I am running Vue.js on my localhost:3000, and Django on localhost:8000. I have set up CORS with django-cors-headers, and I am able to GET requests. However, once I try to DELETE or POST, I get this error:

Forbidden (CSRF cookie not set.)

I understand, that I need to pass a CSRF token in my request's header, which I have:

    deleteImage() {
      const url = this.serverURL + 'images/delete/' + this.image_data.pk;

      const options = {
        method: "DELETE",
        headers: {'X-CSRFToken': this.CSRFtoken}
      };
      fetch(url, options)
        .then(response => {
          console.log(response);
          if (response.ok){ 
            // if response is successful, do something
          }
        })
        .catch(error => console.error(error));
    }

I obtain this.CSRFtoken from a GET request, and the token is the same if I use the approach shown in Django docs.

My Django settings.py looks like this:

rom pathlib import Path
import os

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '***'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',
    'serveImages.apps.ServeimagesConfig',
    'django_admin_listfilter_dropdown',
]

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

CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "http://127.0.0.1:3000"
]


CSRF_TRUSTED_ORIGINS = [
    "http://localhost:3000",
    "http://127.0.0.1:3000"
]

And I know by default django-cors-headers allows the header X-CSRFToken.

I have gone through all previous questions on StackOverflow about this topic, and nothing seems to work.

More context: views.py

from django.http import JsonResponse
import os
from django.conf import settings
from django.middleware import csrf

from .models import Image


def get_csrf_token(request):
    token = csrf.get_token(request)
    return token
    # return JsonResponse({'CSRFtoken': token})

def index(request, dataset, class_label):
    payload = {}

    images_folder_url = os.path.join('static', 'images', dataset, class_label.lower())
    payload['base_url'] = images_folder_url

    data_query = Image.objects.filter(dataset__name=dataset, label__name=class_label).values('pk', 'path', 'isIncluded')
    payload['image_data'] = list(data_query)
    payload['number_of_images'] = len(payload['image_data'])
    payload['CSRFtoken'] = get_csrf_token(request)

    return JsonResponse(payload)

def delete_image(request, img_pk):
    print(request)
    # Just for testing
    return JsonResponse({'status': '200'})

urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('get-token', views.get_csrf_token, name='CSRFtoken'),
    path('images/<str:dataset>/<str:class_label>', views.index, name='index'),
    path('images/delete/<int:img_pk>', views.delete_image, name='delete_image'),
]
Lempres answered 5/7, 2021 at 12:18 Comment(0)
A
6

Okay, so I've been through this battle before and it is frustrating to say the least. If I'm being completely honest, it's because I didn't understand the impetus or interaction of all of the settings involved. Still don't for some of it due to just not having time to read all of the docs. Anyway, there are a lot of potential gotchas and I'll run through a few that I hit that you might be dealing with here.

First, make sure you're running your Vue.js app via the same url. As in, if you're running django on 127.0.0.1:8080 then your Vue app should be running on 127.0.0.1:3000 and not localhost. This probably is not your current issue, but it can give you a wtf moment. If your ultimate setup is to serve your frontend from a different domain than your backend, then you might need to adjust some settings.

Next, enable CORS to allow cookies to be included in cross-site http requests. This probably is not your immediate problem, but it will bite you next.

# https://github.com/adamchainz/django-cors-headers#cors_allow_credentials
CORS_ALLOW_CREDENTIALS = True

Finally, to actually address your current issue, first thing is I would switch to using axios for your requests on the front end.

import axios from 'axios'

axios.defaults.xsrfHeaderName = 'x-csrftoken'
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.withCredentials = true

let djangoURL = 'http://127.0.0.1:8000'
// `timeout` specifies the number of milliseconds before the request times out.
// Because we enable Django Debug Toolbar for local development, there is often
// a processing hit. This can also be tremendously bad with unoptimized queries.
let defaultTimeout = 30000
if (process.env.PROD) {
  djangoURL = 'https://##.##.#.##:###'
  defaultTimeout = 10000
}
axios.defaults.baseURL = djangoURL
axios.defaults.timeout = defaultTimeout

const api = axios.create()
export { api }

The defaultTimeout setting and the conditional local vs prod evaluation are completely optional. It's just a nice to have. Sending your request should be something like:

new Promise((resolve, reject) => {
    api.delete('images/delete/' + this.image_data.pk).then(response => {
      // console.log(response)
      resolve(response)
    }, error => {
      // console.log(error)
      reject(error)
    })
  })

Last thing, set the setting for http only cookies to false

# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly
CSRF_COOKIE_HTTPONLY = False

A good reference example that helped me at times were this blog: https://briancaffey.github.io/2021/01/01/session-authentication-with-django-django-rest-framework-and-nuxt and this blog: https://testdriven.io/blog/django-spa-auth/ The second one sets some settings that are not applicable and it is in react, but still the setup for views is a good start. You're going to ultimately want to incorporate authentication, so choose now; session or jwt. I chose session based auth, but some people use jwt when they want to have authentication through third parties like 0auth and the like.

Example of the views I'm using for auth:

import json
# import logging

from django.contrib.auth import authenticate, login, logout
from django.http import JsonResponse
from django.middleware.csrf import get_token
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_POST
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView

class SessionView(APIView):
    authentication_classes = [SessionAuthentication, BasicAuthentication]
    permission_classes = [IsAuthenticated]

    @staticmethod
    def get(request, format=None):
        return JsonResponse({'isAuthenticated': True})


class WhoAmIView(APIView):
    authentication_classes = [SessionAuthentication, BasicAuthentication]
    permission_classes = [IsAuthenticated]

    @staticmethod
    def get(request, format=None):
        return JsonResponse({'username': request.user.username})


@ensure_csrf_cookie
def get_csrf(request):
    response = JsonResponse({'detail': 'CSRF cookie set'})
    response['X-CSRFToken'] = get_token(request)
    return response


@require_POST
def login_view(request):
    data = json.loads(request.body)
    username = data.get('username')
    password = data.get('password')

    if username is None or password is None:
        return JsonResponse({'detail': 'Please provide username and password.'}, status=400)

    user = authenticate(username=username, password=password)

    if user is None:
        return JsonResponse({'detail': 'Invalid credentials.'}, status=400)

    login(request, user)
    return JsonResponse({'detail': 'Successfully logged in.'})


def logout_view(request):
    if not request.user.is_authenticated:
        return JsonResponse({'detail': 'You\'re not logged in.'}, status=400)

    logout(request)
    return JsonResponse({'detail': 'Successfully logged out.'})

urls.py

urlpatterns = [
    path('csrf/', views.get_csrf, name='api-csrf'),
    path('login/', views.login_view, name='api-login'),
    path('logout/', views.logout_view, name='api-logout'),
    path('session/', views.SessionView.as_view(), name='api-session'),  # new
    path('whoami/', views.WhoAmIView.as_view(), name='api-whoami'),  # new
]

Edit: Another gotcha if you're using Cookiecutter is a limiter that is set for api endpoints. This code below is in the base.py config and if you don't set all of your endpoints under this url they will get this kind of error.

# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
CORS_URLS_REGEX = r"^/api/.*$"

So, either disable this or put your authentication under this url.

Alpenstock answered 25/7, 2021 at 15:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.