How to send CSRF token using Django API and a Flutter-web frontend ? HeaderDisallowedByPreflightResponse [duplicate]

I have a python/django web API with a single endpoint, let's call it /api/v1/form. That API is called from a Flutter-web frontend application. I currently use the following configuration that disables CSRF token verification, and it works :

requirements.txt

Django==5.1.7
django-cors-headers==4.7.0

webserver/settings.py

...

ALLOWED_HOSTS = ["localhost"]
CORS_ALLOWED_ORIGINS = ["http://localhost:8001"] # flutter dev port

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',
    'webserver',
]

...

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

...

webserver/urls.py

from django.urls import path
import webserver.views

urlpatterns = [
    path('api/v1/form', webserver.views.api_v1_form, name="api_v1_form"),
]

...

webserver/views.py

from django.http import HttpResponse, HttpResponseBadRequest

def api_v1_form(request):
  if request.method == "POST":
    process_request(request.body)
    return HttpResponse("request successfully processed.")
  return HttpResponseBadRequest("Expected a POST request")

flutter/lib/page_form.dart

Future<int> sendForm(MyData data) async {
  final response = await http.post(
    Uri.parse("http://localhost:8000/api/v1/form"), 
    body: data.toString(),
  );

  return response.statusCode;
}

Here is what I don't understand : if I disable to the CORS package in order to simply use a vanilla Django server, then I find myself capable of sending requests to the API but unable to receive an answer. Why is that the case ?

The following is the configuration used to get the CSRF token and use it in the requests.

settings.py

ALLOWED_HOSTS = ["localhost"]

CSRF_TRUSTED_ORIGINS = ["http://localhost:8001", "http://localhost:8000"]

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'webserver',
]

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

With only the settings.py changed, I get a 403 forbidden answer.

views.py

from django.views.decorators.csrf import ensure_csrf_cookie

@ensure_csrf_cookie
def api_v1_form(request):
  ... # unchanged

The answer is still 403 forbidden

Lastly, I tried to add a /api/v1/token API to send the token and reuse it in the request.

views.py

def api_v1_token(request):
    if request.method == "GET":
        return HttpResponse(get_token(request))
    return HttpResponseBadRequest()

@ensure_csrf_cookie
def api_v1_form(request):
  if request.method == "OPTIONS": # POST -> OPTIONS
    # ...

page_form.dart

Future<int> envoyerFormulaire(MyData data) async {
  final tokenResponse = await http.get(
    Uri.parse("$apiUrlBase/token"), 
  );
  final token = tokenResponse.body.toString();

  Uri uriPost = Uri.parse("$apiUrlBase/form");
  final dataResponse = await http.post(
    uriPost, 
    body: data.toString(),
    headers: {
      "X-CSRFToken": token,
    }
  );

  return dataResponse.statusCode;
}

However, using the inspector, I get an error indicating the Access-Control-Allow-Origin header is missing when trying to get the token. So I add

views.py

def api_v1_token(request):
    if request.method == "GET":
        response = HttpResponse(get_token(request))
        response["Access-Control-Allow-Origin"] = "http://localhost:8001"
        return response
    return HttpResponseBadRequest()

def api_v1_form(request: HttpResponse):
  if  request.method == "OPTIONS":
    # ...
    response["Access-Control-Allow-Origin"] = "http://localhost:8001"
    return response

Now I can get the token, however the POST request outputs and error PreflightMissingAllowOriginHeader. I get that I am supposed to add a header, but did I not already add Access-Control-Allow-Origin on all requests ?

Moreover I see that for the POST request there are two requests : preflight and fetch, preflight returns 500 internal error because django tries to parse the content of the empty request. That I can fix by adding a filter

views.py

if request.method == "OPTIONS": # preflight
    res = HttpResponse()
    res["Access-Control-Allow-Origin"] = "http://localhost:8001"
    return res

if request.method == "GET":
  # normal processing after that

And now preflight returns 200 OK. But I am left with a CORS error on the fetch request (HeaderDisallowedByPreflightResponse).

Now, I am stuck. I think there is something I don't understand about CORS headers and requests : am I missing headers ? with what values ? and is the app expected to process the data from the preflight and return the result in a subsequent GET request instead of a single POST request ? Is it the expected behavior ?

Back to Top