~

Using CSRF Protection with Django and AJAX Requests

Published: January 03, 2025

Modified: January 03, 2025

Duration: 5 min

Words: 900

Using CSRF Protection with Django and AJAX Requests

Django has built-in support for protection against CSRF by using a CSRF token. It's enabled by default when you scaffold your project using the django-admin startproject <project> command, which adds a middleware in settings.py.

Every POST request to your Django app must contain a CSRF token. In a Django template, you do this by adding {% csrf_token %} to any form that uses the POST method.

Let's see how that can be done with AJAX from a frontend that is separate from Django.

Setup

To show how it's done, we will build a simple app. In the backend, there is a URL of a picture; a GET request will get the picture and a POST request will set a new URL. The app has no error handling and such things in the frontend or backend for simplicity.

Right now it has two endpoints:

  • GET /get-picture - gets the URL of an image stored in the server
  • POST /set-picture - sets the URL of an image stored in the server

And initially the backend code looks like this. I have written all the code in urls.py just to make everything simple.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django.urls import path, include
from django.http import JsonResponse
import json

picture_url = "https://picsum.photos/id/247/720/405"


def get_picture(request):
    print(picture_url)
    return JsonResponse({"picture_url": picture_url})


def set_picture(request):
    if request.method == "POST":
        global picture_url
        picture_url = json.loads(request.body)["picture_url"]
        picture_url = picture_url
        return JsonResponse({"picture_url": picture_url})


urlpatterns = [
    path("get-picture", get_picture),
    path("set-picture", set_picture)
]

Now, for the frontend I'm showing you only the main functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Get the picture my making a GET request
async function get_picture() {
    const res = await fetch("http://localhost:8000/get-picture");
    const data = await res.json();
    const picture_url = data.picture_url;
    return picture_url;
}

// Tries to set the picture_url with a POST request
async function set_picture(picture_url) {
    const res = await fetch("http://localhost:8000/set-picture", {
        method: "POST",
        body: JSON.stringify({ "picture_url": picture_url })
    })
}

And for CORS reasons, you need to set up CORS in the backend, or else you can't make a request. Without CORS configuration in the backend, you will get an error like this.

1
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8000/get-picture. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200.

To fix this, we have to add some headers to the responses. To do this, we will use the django-cors-headers package.

Install and configure django-cors-headers package.

1
pip install django-cors-headers

Configure in settings.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
INSTALLED_APPS = [
    "corsheaders",
    # and more ...
]

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    # and more...
]

CORS_ALLOWED_ORIGINS = ["http://localhost:4040"] # change the port if you use port other than 4040

Now everything for the GET request will work fine. But if you try to set the picture URL, in the backend you will see this error.

1
2
Forbidden (Origin checking failed - http://localhost:4040 does not match any trusted origins.): /set-picture
[06/Jan/2025 13:10:47] "POST /set-picture HTTP/1.1" 403 2554

And this is what we will fix.

Configure CSRF Protection

As your frontend is separate from Django, you need to manually ask for a CSRF token. Django will send the token as a cookie by attaching it in the Set-Cookie headers in the response. The token will be saved in your browser cookie named csrftoken.

First add you frontend URL to CSRF_TRUSTED_ORIGINS in settings.py

1
CSRF_TRUSTED_ORIGINS = ["http://localhost:4040"]

Create a new view to return the CSRF token as a cookie.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from django.views.decorators.csrf import ensure_csrf_cookie

@ensure_csrf_cookie
def get_csrf_token(request):
    return JsonResponse({"success": True})

urlpatterns = [
    # more...,
    path("get-csrf-token", get_csrf_token),
]

Now, we have a way to get the CSRF token from the backend. To, get the token in the frontend add the following code in your backend.

1
2
3
fetch("http://localhost:8000/get-csrf-token", {
    credentials: "include"
});

It makes a request to get a token. With credentials: "include" it tells the browser that if the response header contains any Set-Cookie header, it should obey that instruction. If you remove the credentails property, the cookie won't be set.

If you are curious, look at the network in browser console and check the headers. You will see a header similar to this.

1
Set-Cookie: csrftoken=cyjpe3i9Nrq4yTFfnHjY3n5ekfo7blcu; expires=Mon, 05 Jan 2026 13:22:51 GMT; Max-Age=31449600; Path=/; SameSite=Lax

Modify the set-picture function

Now the only thing we have to do is to send the CSRF token with the set-picture POST call. Modify the set_picture function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
async function set_picture(picture_url) {
    const res = await fetch("http://localhost:8000/set-picture", {
        method: "post",
        credentials: "include",
        headers: {
            'X-CSRFToken': Cookies.get("csrftoken")
        },
        body: JSON.stringify({ "picture_url": picture_url })
    })
}

We need to add an X-CSRFToken header, and its value will be the value of the csrftoken cookie. To get the cookie, the js-cookie library has been used.

Now the POST request should work fine.

There may be other ways to do this. With this method, there are some cons you need to be aware of. If you are deploying your frontend and backend on different domains, there might be a problem with cookies due to browser security and cookie policy. The browser might not set the CSRF cookie, because it will reject any third-party cookie. And even if the security is low, you probably won't be able to read the cookie value with Cookies.get("csrftoken") due to policies related to cookies.

Source Code: https://github.com/sujaudd1n/tutorials/tree/main/django-ajax-csrf

Learn More

  • https://docs.djangoproject.com/en/5.1/howto/csrf/
  • https://docs.djangoproject.com/en/5.1/ref/csrf/
  • https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
  • https://pypi.org/project/django-cors-headers/
  • https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-CSRF_TRUSTED_ORIGINS
  • https://github.com/js-cookie/js-cookie
DjangoSecurity