Ben Holland
Ben Holland

Reputation: 191

Django authentication fails with nginx and gunicorn but works with run_server

I have read countless articles and poured over this problem but I don't have an answer yet. I have a standard system running in docker with docker-compose that has nginx as a reverse proxy to gunicorn and the default django authentication system. I reskinned the login pages by setting form classes but that is all the customization I have.

When I run my website in run_server to debug it, everything works. I go to the login page, log in successfully, and get redirected. is_authenticated produces the excepted results. I run the exact same pages behind nginx and gunicorn and I get very strange behavior. Often I will successfully log in, my sessionid matches the session in the database, the csrf token matches what is in the database but is_authenticated is false. But only about 90% of the time. Often, I will hit a page that requires a login using the login_required decorator on the url (login_required(view.as_view()) and it will have me log in multiple times, is_authenticated is false every time, until it works and when it works, is_authenticated is set to false once past the login page.

This is my nginx configuration. Please note that django is an ip address assigned by docker-compose and gunicorn is running on port 8050. It is really standard for this:

server {
    listen       80;
    server_name  _;

    #charset koi8-r;
    access_log  /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;

    #location / {
    #    root   /usr/share/nginx/html;
    #    index  index.html index.htm;
    #}

    location /static/ {
        autoindex on;
        alias /static/;
    }

    location /pgadmin4/ {
        # forward application requests to the gunicorn server

        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Script-Name /pgadmin4;

        # Changing timeout behavior
        proxy_read_timeout 300;
        proxy_pass http://pgadmin;
    }

    location / {
        # forward application requests to the gunicorn server
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # Changing timeout behavior
        proxy_read_timeout 300;
        proxy_pass http://django:8050;
    }
}

Here is the relevant portion of settings.py

INSTALLED_APPS = [
    'cvi.apps.CviConfig',
    'crispy_forms',
    'notes.apps.NotesConfig',
    'scenarios.apps.ScenariosConfig',
    'tsmodels.apps.TsmodelsConfig',
    'services.apps.ServicesConfig',
    'model_info.apps.ModelInfoConfig',
    'data_tools.apps.DataToolsConfig',
    'demos.apps.DemosConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'bootstrap_datepicker_plus',
    'wkhtmltopdf'
]

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',
]

WSGI_APPLICATION = 'DataManagement.wsgi.application'

And here is the gunicorn run script:

exec gunicorn DataManagement.wsgi:application \
    --name=DataManagement \
    --workers=4 \
    --log-level=debug \
    --bind=0.0.0.0:8050 \
    --timeout=600 \
    --log-file=./gunicorn.log \
    --log-level=debug

This configuration works without authentication. The second that I added login_required to my urls is when there was an authentication problem. I would really appreciate any help you can give. I have never run into this issue before when I set up user authentication on other django websites.

Upvotes: 0

Views: 1096

Answers (1)

Ben Holland
Ben Holland

Reputation: 191

The problem is a lack of documentation and configuration options. We used a SECRET_KEY in Django, obviously not copied here. As per the documentation, we have a get_random_secret_key() for development environments. This does not work with multiple gunicorn workers. Each worker will start a new terminal and thus get a new SECRET_KEY per worker. The secret key is apparently used somewhere in the Django authentication system. You have no control over which worker receives your request from nginx so it is possible that you have a single worker for 3 or 4 of your requests or 4 workers for your 4 requests.

Threads apparently do not inherit the key either so there is really only one solution to use a fixed secret key. Workers start with the same key and it seems to work.

This is not documented in Django or gunicorn and the default working examples will fail for authentication.

Upvotes: 4

Related Questions