When does Django return the csrf token in client's browser ?

I have read that when you open a Django rendered page, Django automatically sends the “csrftoken” in client’s cookies and that if you are rendering a form via Django templates then just use the “{% csrftoken %}” tag to include in the request. But since i am going to be developing the front-end separately, i was just trying out the email authentication views before front-end development begins. But on sending the OTP i am getting this error:

Forbidden (CSRF cookie not set.): /register/
Forbidden (CSRF cookie not set.): /register/
[30/Jan/2025 12:49:34] "POST /register/ HTTP/1.1" 403 16353

On checking cookies in the browser, “csrftoken”, “sessionId” are not there. Why is cookie not being set ? Is it mandatory for the form to be django rendered ?

views.py (URL 127.0.0.1:8000):

def register(request):
    if request.method == "POST":
        # email = request.POST.get("email")
        try:
            # Parse the JSON body
            data = json.loads(request.body)
            email = data.get("email")
        except json.JSONDecodeError:
            return JsonResponse({"success": False, "message": "Invalid JSON format."})

        try:
            validate_email(email)
        except ValidationError:
            return JsonResponse({"success": False, "message": "Invalid email format."})

        email_otp = generate_otp()
        redis_key = f"otp:{email}"

        cache.set(redis_key, email_otp)

        try:
            message = BaseEmailMessage(
                template_name="emails/otp_template.html",
                context={"email_otp": email_otp},
            )
            message.send([email])
        except (BadHeaderError, SMTPException) as e:
            return JsonResponse(
                {"success": False, "message": f"Failed to send OTP. Error: {str(e)}"}
            )

        return JsonResponse(
            {
                "success": True,
                "message": "OTP sent successfully. Please check your email.",
            }
        )

def verify_otp(request):
    if request.method == "POST":
        try:
            # Parse the JSON body
            data = json.loads(request.body)
            email = data.get("email")
            user_otp = data.get("otp")
        except json.JSONDecodeError:
            return JsonResponse({"success": False, "message": "Invalid JSON format."})

        if not email or not user_otp:
            return JsonResponse(
                {"success": False, "message": "Email and OTP are required."}
            )

        redis_key = f"otp:{email}"
        stored_otp = cache.get(redis_key)

        if stored_otp is None:
            return JsonResponse(
                {"success": False, "message": "OTP expired or not found."}
            )

        if validate_otp(stored_otp, user_otp):
            cache.delete(redis_key)
            return JsonResponse(
                {"success": True, "message": "OTP verified successfully."}
            )
        else:
            return JsonResponse({"success": False, "message": "Invalid OTP."})

    return JsonResponse({"success": False, "message": "Invalid request method."})

urls.py:

from django.urls import path
from django.views.generic import TemplateView
from . import views

urlpatterns = [
    path("", TemplateView.as_view(template_name="core/index.html")),
    path("register/", views.register, name="register"),
    path("verify_otp/", views.verify_otp, name="verify_otp"),
]

settings.py:

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

CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = ["http://127.0.0.1:8001", "http://localhost:8001"]

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "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",
]

h.html (URL: 127.0.0.1:8001/h.html):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Register and Verify OTP</title>
</head>
<body>

    <h1>Register</h1>

    <!-- Email Form -->
    <form id="registerForm">
        <label for="email">Email: </label>
        <input type="email" id="email" name="email" required>
        <button type="submit">Send OTP</button>
    </form>

    <!-- OTP Verification Form (Initially hidden) -->
    <div id="otpForm" style="display: none;">
        <h2>Enter OTP</h2>
        <form id="verifyOtpForm">
            <label for="otp">OTP: </label>
            <input type="text" id="otp" name="otp" required>
            <button type="submit">Verify OTP</button>
        </form>
        <div id="otpMessage"></div>
    </div>

    <div id="errorMessage" style="color: red;"></div>

    <script>
        // Function to get CSRF token from cookies
        function getCookie(name) {
            let cookieValue = null;
            if (document.cookie && document.cookie !== "") {
                const cookies = document.cookie.split(";");
                for (let i = 0; i < cookies.length; i++) {
                    const cookie = cookies[i].trim();
                    if (cookie.substring(0, name.length + 1) === (name + '=')) {
                        cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                        break;
                    }
                }
            }
            return cookieValue;
        }

        const csrftoken = getCookie("csrftoken");
        console.log(csrftoken);

        // Handle email registration form submission
        document.getElementById("registerForm").addEventListener("submit", function(event) {
            event.preventDefault(); // Prevent form submission
            let email = document.getElementById("email").value;
            fetch("http://127.0.0.1:8000/register/", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    "X-CSRFToken": csrftoken, // Include CSRF token in the headers
                },
                credentials: "include",
                body: JSON.stringify({ email: email }),
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    // Show OTP form on successful OTP sent
                    document.getElementById("otpForm").style.display = "block";
                    document.getElementById("errorMessage").textContent = ""; // Clear any previous error messages
                } else {
                    // Show error message if OTP wasn't sent successfully
                    document.getElementById("errorMessage").textContent = data.message;
                }
            })
            .catch(error => {
                console.log(error);
                document.getElementById("errorMessage").textContent = "An error occurred while sending OTP.";
            });
        });

        // Handle OTP verification form submission
        document.getElementById("verifyOtpForm").addEventListener("submit", function(event) {
            event.preventDefault(); // Prevent form submission
            let email = document.getElementById("email").value;
            let otp = document.getElementById("otp").value;

            fetch("http://127.0.0.1:8000/verify_otp/", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    "X-CSRFToken": csrftoken, // Include CSRF token in the headers
                },
                credentials: "include",
                body: JSON.stringify({ email: email, otp: otp }),
            })
            .then(response => response.json())
            .then(data => {
                const otpMessageDiv = document.getElementById("otpMessage");
                if (data.success) {
                    otpMessageDiv.style.color = "green";
                    otpMessageDiv.textContent = data.message;
                } else {
                    otpMessageDiv.style.color = "red";
                    otpMessageDiv.textContent = data.message;
                }
            })
            .catch(error => {
                document.getElementById("otpMessage").textContent = "An error occurred during OTP verification.";
            });
        });
    </script>

</body>
</html>

No, but you do need to do a “GET” before your first “POST” in order to get the cookie. What you get doesn’t much matter, and you don’t have to do anything with the html being received. You don’t even need to render a full form.

(Technically, that’s not completely precisely correct - but it’s the easiest way to do it. There’s another thread here from about a year ago where a person was discussing an alternative approach, but I’m not finding it.)

I am making a GET request but still not getting the “csrftoken” back.
added this in h.html:

fetch("http://127.0.0.1:8000/register/", {
            method: "GET",
            credentials: "include",
        })
        .then(response => response.text())
            .then((res) => console.log(res));

views.py:

def register(request):
    if request.method == "POST":
        # email = request.POST.get("email")
        try:
            # Parse the JSON body
            data = json.loads(request.body)
            email = data.get("email")
        except json.JSONDecodeError:
            return JsonResponse({"success": False, "message": "Invalid JSON format."})

        try:
            validate_email(email)
        except ValidationError:
            return JsonResponse({"success": False, "message": "Invalid email format."})

        email_otp = generate_otp()
        redis_key = f"otp:{email}"

        cache.set(redis_key, email_otp)

        try:
            message = BaseEmailMessage(
                template_name="emails/otp_template.html",
                context={"email_otp": email_otp},
            )
            message.send([email])
        except (BadHeaderError, SMTPException) as e:
            return JsonResponse(
                {"success": False, "message": f"Failed to send OTP. Error: {str(e)}"}
            )

        return JsonResponse(
            {
                "success": True,
                "message": "OTP sent successfully. Please check your email.",
            }
        )

    return render(request, "core/empty.html")

empty.html:

<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
</body>
</html>

but still getting:

[31/Jan/2025 03:47:47] "GET /register/ HTTP/1.1" 200 71
Forbidden (CSRF cookie not set.): /register/
Forbidden (CSRF cookie not set.): /register/
[31/Jan/2025 03:47:58] "POST /register/ HTTP/1.1" 403 2855

browser console:

<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
</body>
</html>

h.html:64 
               
 POST http://127.0.0.1:8000/register/ 403 (Forbidden)
(anonymous) @ h.html:64
h.html:85 SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON

I got the token once by a fluke, i wanted to know what changed and it got sent, so i deleted it and checked if it would be sent again but haven’t been able to reproduce it.

Additional Info:
settings/common.py:

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "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",
]

settings/dev.py:

if DEBUG:
    # MIDDLEWARE += ["silk.middleware.SilkyMiddleware"]
    # INSTALLED_APPS += ["debug_toolbar"]
    # MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
    INTERNAL_IPS = ["127.0.0.1"]

What you’re returning from the server in your “dummy view” still needs to contain the token.

Your response needs to include {% csrf_token %} if you’re rendering HTML. Or, your view could return a JSONResponse where the token is obtained by calling the get_token method in django.middleware.csrf. In either case, it’s the responsibility of your JavaScript to extract that value from the response for it to be included in your POST.

You may also want to spend some time reading:

to understand what this all is doing.

I now understand, thanks! But I noticed that no one really talks about the fact that you need to make a GET request before a POST to retrieve the CSRF token. Then, depending on the approach, you either include {% csrf_token %} in the template returned from the server or send a JSON response with the token. Even the documentation you mentioned doesn’t explicitly state this. Maybe since I primarily work on the backend and have little experience with frontend development, I was struggling with this issue. Is it something that’s just assumed to be common knowledge, or was it overlooked?

Also do i need to mark this thread as solved or something ?