From 1865de948eca4f3ade5db091dac1fa3ebfd41649 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 13 Aug 2026 21:41:47 +0200 Subject: [PATCH] Fix redirect-lus achter TLS-proxy (v0.2.02 beta) Inloggen mislukte met DJANGO_SECURE=1 achter een reverse proxy die TLS afhandelt, terwijl het inlogscherm zelf wel laadde. - nginx.conf gaf `X-Forwarded-Proto $scheme` door. Achter een TLS-proxy komt het verzoek als http binnen, dus overschreef nginx de `https` van de proxy met `http`. Django zag de verbinding daardoor als onveilig en stuurde elk verzoek naar /api en /admin door naar https, waarna de proxy het weer als http aanleverde: een oneindige redirect-lus. - nginx neemt nu de X-Forwarded-Proto van de proxy over via een map, en valt alleen terug op $scheme als die header ontbreekt. Werkt daardoor zowel achter een externe proxy als bij TLS op nginx zelf. - .env.example: voorbeelden voor CSRF-origin en frontend-URL op https gezet, DJANGO_SECURE standaard op 1, met uitleg waarom een http-origin het inloggen met een CSRF-fout laat stranden. - DEPLOY.md: checklist "Draaien achter TLS" toegevoegd met de drie vereisten (proxy-header, CSRF-origin, allowed hosts), commando's om te controleren of de header aankomt, en het advies poort 8080 aan de loopback te binden zodat de app niet buiten TLS om bereikbaar is. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FAQ4Np13v8fwbTtKHKnwDz --- .env.example | 13 ++++++++++--- DEPLOY.md | 38 ++++++++++++++++++++++++++++++++++++++ frontend/nginx.conf | 16 ++++++++++++++-- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- frontend/src/modern.css | 2 +- 6 files changed, 66 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 6dd5427..261f3fd 100644 --- a/.env.example +++ b/.env.example @@ -15,11 +15,17 @@ DJANGO_DEBUG=0 DJANGO_ALLOWED_HOSTS=rooster-test.example.nl,localhost,127.0.0.1 # Volledige origin(s) inclusief http(s):// voor CSRF achter nginx. -DJANGO_CSRF_TRUSTED_ORIGINS=http://rooster-test.example.nl:8080 +# LET OP: dit moet exact de origin zijn die de browser gebruikt. Draai je met +# TLS, dan hoort hier https:// te staan (en zonder poort als je op 443 zit). +# Staat hier http:// terwijl de browser https:// gebruikt, dan mislukt elke +# POST — dus ook het inloggen — met een CSRF-fout. +DJANGO_CSRF_TRUSTED_ORIGINS=https://rooster.example.nl # Zet op 1 zodra de server achter HTTPS draait (TLS-certificaat aanwezig). # Activeert o.a. veilige cookies, HSTS en een redirect naar https. -DJANGO_SECURE=0 +# Vereist dat de reverse proxy X-Forwarded-Proto: https meestuurt; nginx.conf +# in dit project geeft die header door. +DJANGO_SECURE=1 # --- PostgreSQL ----------------------------------------------------------- POSTGRES_DB=rooster @@ -37,5 +43,6 @@ EMAIL_HOST_PASSWORD= EMAIL_USE_TLS=1 DEFAULT_FROM_EMAIL=Roosterwijs # Basis-URL van de frontend voor resetlinks in e-mails (zonder slash op het eind), -# bv. http://rooster-test.example.nl:8080 (leeg = afleiden uit het verzoek). +# bv. https://rooster.example.nl (leeg = afleiden uit het verzoek). +# Met TLS: gebruik https://, anders wijzen resetlinks naar een http-adres. FRONTEND_BASE_URL= diff --git a/DEPLOY.md b/DEPLOY.md index dd45466..1a1ae55 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -146,6 +146,44 @@ Migraties draaien automatisch mee bij het opstarten van de backend. je breidt de nginx-config uit met certificaten. De nginx-container is daar al het logische punt voor. **Zet daarna `DJANGO_SECURE=1` in `.env`** — dat activeert veilige cookies, HSTS en een https-redirect in Django. + + Zie hieronder wat er dan verder moet kloppen. + +### Draaien achter TLS: checklist + +Met `DJANGO_SECURE=1` moeten drie dingen kloppen, anders lukt **inloggen niet** +terwijl het inlogscherm zelf gewoon laadt (dat wordt door nginx geserveerd, +zonder tussenkomst van Django). + +1. **De proxy moet `X-Forwarded-Proto: https` meesturen.** Django leest die + header (`SECURE_PROXY_SSL_HEADER`) om te bepalen of de verbinding veilig is. + Ontbreekt hij, dan denkt Django dat het http is en stuurt het elk verzoek + naar `/api` en `/admin` door naar https — waarna de proxy het weer als http + aanlevert: een oneindige redirect-lus (`ERR_TOO_MANY_REDIRECTS`) en dus een + mislukte login. Caddy en Traefik zetten deze header standaard; controleer het + als je een eigen nginx/Apache ervoor hebt. +2. **`DJANGO_CSRF_TRUSTED_ORIGINS` moet de https-origin zijn**, exact zoals de + browser hem gebruikt (bv. `https://rooster.example.nl`, zonder poort op 443). + Staat hier nog een `http://`-adres, dan geeft elke POST — inclusief het + inloggen — een CSRF-fout (HTTP 403). +3. **`DJANGO_ALLOWED_HOSTS` moet de publieke hostnaam bevatten**, anders volgt + een 400 Bad Request. + +Controleren of de header goed aankomt: + +```bash +docker compose logs backend | grep -i "Mislukte inlogpoging" # bereikt de login de backend? +curl -sI https:///api/auth/me/ | head -n 1 # 200/401 = goed, 301 = lus +``` + +Krijg je bij die `curl` een `301`, dan komt `X-Forwarded-Proto` niet goed door. + +- **Beperk poort 8080 tot de proxy:** in `docker-compose.yml` staat + `"8080:80"`, wat op álle netwerkinterfaces luistert. Daardoor is de app ook + rechtstreeks via `http://:8080` te bereiken — buiten TLS om. Draait je + reverse proxy op dezelfde machine, maak er dan `"127.0.0.1:8080:80"` van. + Draait de proxy op een andere host, laat het dan staan en zet er een firewall + op: nginx vertrouwt de `X-Forwarded-Proto` van de aanroeper. - **Inloggen verplicht:** de hele API vereist een ingelogde gebruiker; de frontend toont eerst een inlogscherm. Maak gebruikers aan via `createsuperuser` (zie boven) of in de Django-admin. Modulebeheer is diff --git a/frontend/nginx.conf b/frontend/nginx.conf index e6791f7..0921fe2 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -1,4 +1,16 @@ # Nginx: serveert de React-app en stuurt API/admin door naar gunicorn. + +# Draait er een reverse proxy vóór ons die TLS afhandelt (Caddy/Traefik), dan +# komt het verzoek hier als gewoon http binnen en is $scheme dus "http". Zouden +# we dat doorgeven, dan denkt Django dat de verbinding onveilig is en stuurt het +# met DJANGO_SECURE=1 elk verzoek door naar https — een oneindige redirect-lus. +# Daarom: neem de X-Forwarded-Proto van de proxy over, en val alleen terug op +# $scheme als die header ontbreekt (nginx zelf aan de buitenkant, of geen TLS). +map $http_x_forwarded_proto $rw_forwarded_proto { + default $http_x_forwarded_proto; + "" $scheme; +} + server { listen 80; server_name _; @@ -33,7 +45,7 @@ server { 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-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $rw_forwarded_proto; } # Django-admin loopt ook via de backend. @@ -42,7 +54,7 @@ server { 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-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $rw_forwarded_proto; } # Alle overige paden: de single-page React-app. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 32ce625..d85f209 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "roosterwijs-frontend", - "version": "0.2.1-beta", + "version": "0.2.2-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roosterwijs-frontend", - "version": "0.2.1-beta", + "version": "0.2.2-beta", "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/frontend/package.json b/frontend/package.json index bdb6bbd..3b11bb9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "roosterwijs-frontend", "private": true, - "version": "0.2.1-beta", + "version": "0.2.2-beta", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/modern.css b/frontend/src/modern.css index aef100c..e826f65 100644 --- a/frontend/src/modern.css +++ b/frontend/src/modern.css @@ -35,7 +35,7 @@ button,a,input,select{transition:border-color .15s,background-color .15s,color . .cog-btn{width:100%;min-height:38px;justify-content:flex-start;font-size:15px;padding:0} .cog-btn::after{content:"Beheer en instellingen";margin-left:8px;font-size:13px} .cog-dropdown{position:static;min-width:0;margin:4px 0 0;box-shadow:none} -.topbar::after{content:"v0.2.01 beta";order:4;margin:10px 8px 0;color:#98a2b3;font-size:10px} +.topbar::after{content:"v0.2.02 beta";order:4;margin:10px 8px 0;color:#98a2b3;font-size:10px} .content{margin-left:var(--sidebar);width:calc(100% - var(--sidebar));max-width:1680px;padding:32px 38px 48px} .content-wide{padding:24px 28px 42px} h1{font-size:clamp(24px,2.4vw,30px);line-height:1.2;letter-spacing:-.025em;font-weight:750}