Python Django: How to Resolve Error "CSRF Token Mismatch"
The "CSRF token mismatch" error is one of the most common issues developers encounter when building web applications with Django. While CSRF protection is essential for securing your application against malicious attacks, misconfiguration or missing tokens can block legitimate requests and leave you staring at a 403 Forbidden page.
In this guide, you'll learn what CSRF is, why Django enforces it, what causes the token mismatch error, and how to fix it with practical examples.
What Is CSRF?
CSRF (Cross-Site Request Forgery) is a type of web security attack where a malicious website tricks a user's browser into making an unwanted request to a different site where the user is already authenticated. For example, a hidden form on an attacker's page could submit a money transfer request to your banking site using your active session.
To prevent this, Django generates a unique, random CSRF token for each user session. This token must be included in every state-changing request (POST, PUT, DELETE). When the server receives the request, it compares the submitted token against the one stored in the session. If they don't match or if the token is missing, Django rejects the request.
What Causes the "CSRF Token Mismatch" Error?
The error occurs when Django's CsrfViewMiddleware determines that the CSRF token in the incoming request does not match the expected token for the current session. The most common causes are:
- Missing
{% csrf_token %}tag in your HTML form template. - CSRF middleware is not enabled in your Django settings.
- Expired session or token: the user was inactive for too long and the session expired.
- Cross-domain requests: submitting a form or AJAX request to a different domain without proper CSRF configuration.
- Stale or corrupted browser cookies: old cookies conflict with the current session token.
- AJAX requests missing the CSRF header: JavaScript requests don't automatically include the CSRF token.
- Caching issues: a cached page serves an outdated CSRF token.
How Django CSRF Protection Works
Understanding the flow helps you debug the mismatch:
- When a user visits a page, Django sets a
csrftokencookie in the browser. - When the page contains a form with
{% csrf_token %}, Django renders a hidden input field containing the token value. - When the form is submitted, Django's
CsrfViewMiddlewarecompares the token from the form data (or theX-CSRFTokenheader) against the token in the cookie. - If they match, the request proceeds. If not, Django returns a 403 Forbidden response with the message: "CSRF verification failed. Request aborted."
Fix 1: Include the CSRF Token in Your Form
The most common cause of this error is a missing {% csrf_token %} tag in your template.
Wrong: Missing CSRF token:
<form method="POST" action="/login/">
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">Log In</button>
</form>
Submitting this form will produce:
Forbidden (403)
CSRF verification failed. Request aborted.
Correct: CSRF token included:
<form method="POST" action="/login/">
{% csrf_token %}
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">Log In</button>
</form>
When you inspect the rendered HTML, you should see a hidden input field:
<input type="hidden" name="csrfmiddlewaretoken" value="a1b2c3d4e5f6...">
You can verify the token is present by right-clicking the form in your browser, selecting Inspect, and looking for the csrfmiddlewaretoken hidden input.
Fix 2: Ensure CSRF Middleware Is Enabled
Django's CSRF protection is handled by CsrfViewMiddleware. If it's missing from your middleware configuration, tokens won't be validated, but they also won't be generated properly, which can cause confusing behavior.
Verify that your settings.py includes the middleware:
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # Must be present
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Never remove CsrfViewMiddleware from production settings to "fix" the error. This disables CSRF protection entirely and leaves your application vulnerable to cross-site request forgery attacks.
Fix 3: Include the CSRF Token in AJAX Requests
When making AJAX requests with JavaScript (e.g., using fetch or XMLHttpRequest), the CSRF token is not automatically included. You must extract it from the cookie and add it as a request header.
Using vanilla JavaScript with fetch:
// Helper function to read a cookie by name
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.startsWith(name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// Include the CSRF token in the request header
fetch('/api/update/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'), // Required
},
body: JSON.stringify({ key: 'value' }),
})
.then(response => response.json())
.then(data => console.log(data));
Using jQuery (shortcut with $.ajaxSetup):
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
}
}
});
Django's documentation provides the getCookie helper function. You can also access the token directly from the DOM if you've rendered it in a <meta> tag:
<meta name="csrf-token" content="{{ csrf_token }}">
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
Fix 4: Clear Browser Cookies
Stale or corrupted cookies can cause a mismatch between the token stored in the cookie and the one rendered in the form. Clearing cookies for your domain often resolves this.
Steps:
- Open your browser's Settings (or Preferences).
- Navigate to Privacy & Security → Cookies and site data.
- Search for your domain (e.g.,
localhostor your production domain). - Delete the cookies associated with that domain.
- Refresh the page and try again.
Alternatively, you can clear cookies directly from Developer Tools:
- Open Developer Tools (
F12orCtrl+Shift+I). - Go to the Application tab (Chrome) or Storage tab (Firefox).
- Under Cookies, select your domain and delete the
csrftokencookie. - Reload the page.
Fix 5: Handle Expired Sessions
CSRF tokens are tied to the user's session. If the session expires (due to inactivity or a server restart), the token becomes invalid. The next form submission will fail with a mismatch error.
Solutions:
- Increase session timeout in
settings.py:
# settings.py
# Session expires after 24 hours (in seconds)
SESSION_COOKIE_AGE = 86400
# Keep the session alive as long as the user is active
SESSION_SAVE_EVERY_REQUEST = True
- Handle the error gracefully by catching the 403 response and prompting the user to refresh:
# views.py
from django.middleware.csrf import CsrfViewMiddleware
from django.http import HttpResponseForbidden
# In your custom 403 handler:
def csrf_failure(request, reason=""):
return HttpResponseForbidden(
"Your session has expired. Please refresh the page and try again."
)
Register the custom handler in settings.py:
# settings.py
CSRF_FAILURE_VIEW = 'myapp.views.csrf_failure'
Fix 6: Configure CSRF for Cross-Domain Requests
If your frontend and backend are on different domains (e.g., a React frontend on localhost:3000 and a Django API on localhost:8000), you need to configure Django to accept CSRF tokens from the frontend's origin.
# settings.py
# Allow the frontend domain to send CSRF cookies
CSRF_TRUSTED_ORIGINS = [
'http://localhost:3000',
'https://yourfrontend.com',
]
# If using cookies across domains
CSRF_COOKIE_SAMESITE = 'Lax' # or 'None' for cross-site (requires HTTPS)
CSRF_COOKIE_HTTPONLY = False # Must be False so JavaScript can read the token
Setting CSRF_COOKIE_SAMESITE = 'None' requires CSRF_COOKIE_SECURE = True, which means your site must be served over HTTPS. Browsers will reject SameSite=None cookies without the Secure flag.
Fix 7: Temporarily Disable CSRF for Debugging (Development Only)
If you need to isolate whether CSRF is the actual cause of your issue, you can temporarily exempt a specific view using the @csrf_exempt decorator:
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
@csrf_exempt
def my_api_view(request):
if request.method == 'POST':
return JsonResponse({"status": "success", "message": "CSRF bypassed"})
return JsonResponse({"status": "ready"})
Never use @csrf_exempt in production unless the view is explicitly designed to receive requests from external services (e.g., webhooks) and you have implemented alternative authentication (such as API tokens or HMAC signatures). Disabling CSRF protection exposes the endpoint to cross-site request forgery attacks.
Debugging Checklist
If you're still encountering the error after trying the fixes above, work through this checklist:
| Check | How to Verify |
|---|---|
{% csrf_token %} in form template | Inspect the rendered HTML for the hidden input |
CsrfViewMiddleware in MIDDLEWARE | Review settings.py |
| CSRF token in AJAX headers | Check the X-CSRFToken header in Developer Tools → Network tab |
csrftoken cookie exists | Developer Tools → Application → Cookies |
| Cookie not expired | Delete and regenerate by refreshing the page |
| Cross-domain origins trusted | Check CSRF_TRUSTED_ORIGINS in settings.py |
| No caching of CSRF-protected pages | Ensure Cache-Control: no-cache headers are set |
| JavaScript console errors | Developer Tools → Console tab |
Conclusion
The "CSRF token mismatch" error in Django is a security feature working as intended, i.e. it's telling you that the token submitted with the request doesn't match what the server expects. In most cases, the fix is straightforward:
- Add
{% csrf_token %}to every POST form in your templates. - Include the
X-CSRFTokenheader in AJAX requests. - Clear stale browser cookies if the token has become corrupted.
- Configure
CSRF_TRUSTED_ORIGINSfor cross-domain setups.
Resist the temptation to disable CSRF protection globally. Instead, understand the flow (cookie, hidden field, middleware comparison) and you'll be able to diagnose and fix any CSRF-related issue quickly and securely.