Handling CSRF Login Errors Gracefully in Django

What's CSRF? Cross site request forgery is a type of attack where a malicious website tricks a user into performing actions on another site where they're authenticated. This is usually done by embedding a form in the malicious site, and submitting it to the target site. An example of this would be a card game website where, when you hit the "Play" button, it sends a POST request to another site with the payload to change your login email address to the attacker's. Since you're logged in to the target site, the request goes through and you lose access to your account. How does it work in Django By default, Django servers you a cookie with the CSRF token on the first request. This token (in a masked form) is embedded in every form that Django generates, and is unique to the user and the session. The form token is checked on every unsafe request (POST, PUT, DELETE, PATCH). If the token is missing, invalid, or does not match the token in the cookie, the server responds with a 403 Forbidden response. This way Django ensures that the request is coming from the site itself, and not from a malicious third party, since no other server can generate valid CSRF tokens. The problem The scenario is as follows: You open a website in one tab You open the same website in another tab You log in in the second tab, and start using the website You go back to the first tab, and try to do something that requires a POST request (like submitting a form) You get a 403 Forbidden CSRF Error response For security reasons, Django cycles CSRF tokens on every login. This means that the token embedded in the form in the first tab is now invalid since it was generated before your login in the second tab. Django, being the best web framework out there, even warns you about this if you have DEBUG = True and you get a CSRF failure. Since this can happen to regular users, it's not just a security problem, but also a UX problem. The users are most likely to encounter it on the login page because it is one of the few public forms every site has, and a successful login cycles the token. Solution #1: Pure Django solution Django allows setting a custom CSRF failure handler view via settings.CSRF_FAILURE_VIEW variable. For a seamless UX, in case this happens on the login view, you could redirect the user back to the referrer page. Since they're already logged in, they will be able to access it. As a bonus, let's add nice template for the CSRF failure view that explains what happened and offers a button to go back to the previous page. # settings.py CSRF_FAILURE_VIEW = 'myapp.views.csrf_failure' # views.py from http import HTTPStatus from django.shortcuts import redirect, render def csrf_failure(request, reason=""): referer = request.META.get("HTTP_REFERER", "/") if resolve(request.path).url_name == "login": return redirect(referer) return render(request, "csrf_failure.html", context={"referer": referer}, status=HTTPStatus.FORBIDDEN) Solution #2: Javascript Another solution would be to use Javascript to periodically check if the CSRF cookie changed since the initial page load and warn the user if it did. // csrf.js const COOKIE_NAME = 'csrftoken'; function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); } function checkCSRFChange() { const currentToken = getCookie(COOKIE_NAME); if (currentToken && currentToken !== initialToken) { alert("Your session has changed or expired. Please reload the page to avoid losing changes."); } } const initialToken = getCookie(COOKIE_NAME); setInterval(checkCSRFChange, 5000);

Mar 22, 2025 - 14:07
 0
Handling CSRF Login Errors Gracefully in Django

What's CSRF?

Cross site request forgery is a type of attack where a malicious website tricks a user into performing actions on another site where they're authenticated. This is usually done by embedding a form in the malicious site, and submitting it to the target site.

An example of this would be a card game website where, when you hit the "Play" button, it sends a POST request to another site with the payload to change your login email address to the attacker's. Since you're logged in to the target site, the request goes through and you lose access to your account.

How does it work in Django

By default, Django servers you a cookie with the CSRF token on the first request. This token (in a masked form) is embedded in every form that Django generates, and is unique to the user and the session.

The form token is checked on every unsafe request (POST, PUT, DELETE, PATCH). If the token is missing, invalid, or does not match the token in the cookie, the server responds with a 403 Forbidden response.

This way Django ensures that the request is coming from the site itself, and not from a malicious third party, since no other server can generate valid CSRF tokens.

The problem

The scenario is as follows:

  1. You open a website in one tab
  2. You open the same website in another tab
  3. You log in in the second tab, and start using the website
  4. You go back to the first tab, and try to do something that requires a POST request (like submitting a form)
  5. You get a 403 Forbidden CSRF Error response

For security reasons, Django cycles CSRF tokens on every login. This means that the token embedded in the form in the first tab is now invalid since it was generated before your login in the second tab.

Django, being the best web framework out there, even warns you about this if you have DEBUG = True and you get a CSRF failure.

Image description

Since this can happen to regular users, it's not just a security problem, but also a UX problem. The users are most likely to encounter it on the login page because it is one of the few public forms every site has, and a successful login cycles the token.

Solution #1: Pure Django solution

Django allows setting a custom CSRF failure handler view via settings.CSRF_FAILURE_VIEW variable. For a seamless UX, in case this happens on the login view, you could redirect the user back to the referrer page. Since they're already logged in, they will be able to access it.

As a bonus, let's add nice template for the CSRF failure view that explains what happened and offers a button to go back to the previous page.

# settings.py
CSRF_FAILURE_VIEW = 'myapp.views.csrf_failure'

# views.py
from http import HTTPStatus

from django.shortcuts import redirect, render

def csrf_failure(request, reason=""):
    referer = request.META.get("HTTP_REFERER", "/")
    if resolve(request.path).url_name == "login":
        return redirect(referer)

    return render(request, "csrf_failure.html", context={"referer": referer}, status=HTTPStatus.FORBIDDEN)

Solution #2: Javascript

Another solution would be to use Javascript to periodically check if the CSRF cookie changed since the initial page load and warn the user if it did.

// csrf.js
const COOKIE_NAME = 'csrftoken';

function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

function checkCSRFChange() {
    const currentToken = getCookie(COOKIE_NAME);
    if (currentToken && currentToken !== initialToken) {
        alert("Your session has changed or expired. Please reload the page to avoid losing changes.");
    }
}

const initialToken = getCookie(COOKIE_NAME);
setInterval(checkCSRFChange, 5000);