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
command, which adds a middleware in settings.py.
startproject <project>
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.
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.
// 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.
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.
pip install django-cors-headers
Configure in settings.py
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.
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
CSRF_TRUSTED_ORIGINS = ["http://localhost:4040"]
Create a new view to return the CSRF token as a cookie.
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.
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.
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.
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
Top comments (0)