Getting Forbidden (CSRF cookie not set.): while trying to login to Django Admin Page

Hi, I’ve already searched a lot and tried a lot of things, but did not came up with a solution yet.
When accessing my development environment via localhost/127.0.0.1 everything works fine, standard django admin login, and all my forms, but when I access via my host IP I get the 403 Forbidden with every Form POST.


settings.py

import os
import environ
from pathlib import Path

# Set the project base directory
BASE_DIR = Path(__file__).resolve().parent.parent

env = environ.Env(
    DEBUG=(bool, False)
)
# Take environment variables from .env file
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
#Environ
SECRET_KEY = env('SECRET_KEY')
DEBUG = env('DEBUG')
ALLOWED_HOSTS = env('ALLOWED_HOSTS').split(' ')
SECURE_HSTS_SECONDS = env('SECURE_HSTS_SECONDS')
SECURE_HSTS_INCLUDE_SUBDOMAINS = env('SECURE_HSTS_INCLUDE_SUBDOMAINS')
SECURE_HSTS_PRELOAD = env('SECURE_HSTS_PRELOAD')
CSRF_COOKIE_SECURE = env('CSRF_COOKIE_SECURE')
SESSION_COOKIE_SECURE = env('SESSION_COOKIE_SECURE')
CSRF_TRUSTED_ORIGINS = ['http://*', 'https://*']
ALLOWED_ORIGINS = CSRF_TRUSTED_ORIGINS.copy()

urls.py

urlpatterns += [
    path('accounts/', include('django.contrib.auth.urls')),
    path('', TemplateView.as_view(template_name='home.html'), name='home'),
    path('registro/', include('registro.urls'), name='registro'),
]

.env I’m using exclusively on my development environment:

export DEBUG=True
export ALLOWED_HOSTS='*'
export SECURE_HSTS_SECONDS=15780000
export SECURE_SSL_REDIRECT=False
export SECURE_HSTS_INCLUDE_SUBDOMAINS=False
export SECURE_HSTS_PRELOAD=False
export CSRF_COOKIE_SECURE=False
export SESSION_COOKIE_SECURE=False

But I’ve already tried, with no sucess, to test obeying to all ./manage.py check --deploy requirements

Extra info: currently running manage.py runserver 0.0.0.0:8080
Also tried running via uvicorn, uvicorn+nginx, nothing worked so far.

Thanks in advance.

1 Like

The error message specifically identifies 5 steps to take. Have you verified those 5 items?

If you believe that you have, then look at the network tab in your browser’s developer tools and retry the login. Inspect the GET and POST requests to see if the CSRF cookie was retrieved on the GET and being returned in the POST.

Thanks for your answer @KenWhitesell . Yep I have checked all 5.
I have allowed my browser to accept cookies from it:
Template and view are Django’s standard so…it should work.
I have spotted this error first on a custom view/form of mine, and then decided to try Django’s standard and it also happened.
Also csrfMiddlewhere is being used in my settings.py

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    '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',
]

It looks like the cookie hasnt been retrieved on the GET on my Firefox.

But have been on my Chromium:


But still fails on POST anyway:

From what I can see here, the value of the token in the cookie does not match the value of the token being submitted in the post.

Have you done any customization of the login process? (Custom template or view?) If so, can you post it?

What is your LOGIN_URL setting?

True. Haven’t noticed that the tokens differ.
I don’t have a LOGIN_URL setting on my settings.py but, I have:

LOGIN_REDIRECT_URL = "home"

I have a custom user model. But haven’t changed a thing regarding login for this test, using default Django Admin template and view.

Custom user model:

class UserManager(BaseUserManager):
    def create_user(self, email, password=None):
        if not email:
            raise ValueError('Users must have an email address')
        user = self.model(email=self.normalize_email(email))
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None):
        user = self.create_user(
            email,
            password=password,
        )
        user.is_admin = True
        user.save(using=self._db)
        return user


class Piloto(AbstractBaseUser, PermissionsMixin):
    primeiro_nome = models.CharField(max_length=100, default=None, null=True)
    ultimo_nome = models.CharField(max_length=100, default=None, null=True)
    email = models.EmailField(verbose_name='E-mail address', max_length=100, unique=True)
    discord_id = models.CharField(max_length=30, unique=True, default=None, null=True)
    password = models.CharField(max_length=100)
    data_de_nascimento = models.DateField(null=True)
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    USERNAME_FIELD = 'email'

    objects = UserManager()

    def __str__(self):
        return self.email

    def has_perm(self, perm, obj=None):
        # "Does the user have a specific permission?"
        # Simplest possible answer: Yes, always
        return True

    def has_module_perms(self, app_label):
        # "Does the user have permissions to view the app `app_label`?"
        # Simplest possible answer: Yes, always
        return True

    @property
    def is_staff(self):
        # "Is the user a member of staff?"
        # Simplest possible answer: All admins are staff
        return self.is_admin

The “home” template:

type or paste code here

<!-- templates/home.html-->
{% extends 'base.html' %}

{% block title %}Home{% endblock %}

{% block content %}
{% if user.is_authenticated %}
<div class="container py-5 w-50 text-center">
    Bem-vindo {{ user.primeiro_nome }}!
    <p><a class="btn-link" href="{% url 'logout' %}">Log Out</a></p>
  {% else %}
    <p>You are not logged in</p>
    <a class="btn-link" href="{% url 'login' %}">Log In</a>
    {% if messages %}
		{% for message in messages %}
			{% if message.tags == 'success' %}
				<div class="alert alert-success" role="alert">
					<p>{{ message|safe }}</p>
				</div>
			{% endif %}
		{% endfor %}
	{% endif %}
</div>
{% endif %}
{% endblock %}

Tested on localhost to see if GET and POST tokens differ:

So that’s potentially misleading information that you’re looking at here.

The request token should match what was returned in the previous response that resulted in the page being sent.

What you get as the token in the response should match what the next request submits.

In other words, the sequence is going to be something like:

GET /login
(Page returns “token 1”)

POST /login
(Request returns “token 1”)
(Response returns “token 2”)

1 Like

I have been seeing this a lot lately with Django 4.2 I’m in the admin. The fix is normally to set CSRF_TRUSTED_ORIGINS to a valid domain, and I don’t think you can use * like you are using.

Trying setting it to your domain: CSRF_TRUSTED_ORIGINS = ['http://yourdomain.com', 'https://yourdomain.com', 'http://*.yourdomain.com', 'https://*.yourdomain.com']

If that works, you can use CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS, default=[]) and then set ENV variables to keep using that pattern if you want.

If you changed the value of ALLOWED_HOSTS I would set it back to your * default until things are working. I don’t think you have to specify “http://” or “https://” with it.

1 Like

See Carlton’s newly published / better solution: CSRF and Trusted Origins in Django 4.x+

2 Likes

Ha! Thanks @jeff — yes, that looks on topic. :gift: