diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index f44875f..03d2c1f 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1019,25 +1019,7 @@ server { } # WebSocket-Routen /ws/chat und /ws/voice - location /ws/ { - auth_request /authelia-check; - auth_request_set $user $upstream_http_remote_user; - error_page 401 =302 https://auth.jamulix.de/?rd=$scheme://$http_host$request_uri; - - proxy_pass http://127.0.0.1:8003; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For ""; - proxy_set_header Remote-User $user; - proxy_read_timeout 300s; - proxy_send_timeout 300s; - } - - # Admin-Live-Log (WebSocket) — Auth wie unten, aber mit WS-Upgrade wie /ws/. - # OHNE diesen Block bleibt der Log-Tab im Admin-Panel leer ("Verbindungsfehler"), - # weil /api/admin/log sonst in location / ohne Upgrade-Header landet. + # Admin-Live-Log (WebSocket) — hinter Authelia, mit WS-Upgrade. location = /api/admin/log { auth_request /authelia-check; auth_request_set $user $upstream_http_remote_user; @@ -1054,11 +1036,11 @@ server { proxy_send_timeout 300s; } - # Alle anderen Routen — Authelia 2FA-Schutz - location / { + # Admin-API — ZUSÄTZLICH hinter Authelia (Doppelschutz). Identität via Remote-User. + location /api/admin/ { auth_request /authelia-check; auth_request_set $user $upstream_http_remote_user; - error_page 401 =302 https://auth.jamulix.de/?rd=$scheme://$host$request_uri; + error_page 401 =302 https://auth.jamulix.de/?rd=$scheme://$http_host$request_uri; proxy_pass http://127.0.0.1:8003; proxy_http_version 1.1; @@ -1068,6 +1050,33 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Remote-User $user; } + + # Sprach-/Chat-WebSockets — KEIN Authelia. Auth via Token-Cookie (Senioren-Link). + # Remote-User HART LEEREN: sonst könnte ein Client per Header eine Identität + # vortäuschen (die App vertraut Remote-User von der Proxy-IP 127.0.0.1). + location /ws/ { + proxy_pass http://127.0.0.1:8003; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For ""; + proxy_set_header Remote-User ""; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + # App (Senioren) — KEIN Authelia. Auth via Token-Cookie / Ein-Klick-Link + # (https://voice.jamulix.de/?k=). Remote-User HART LEEREN (Anti-Spoofing). + location / { + proxy_pass http://127.0.0.1:8003; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For ""; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Remote-User ""; + } } ``` diff --git a/app/main.py b/app/main.py index 53d7617..bea8e60 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from pathlib import Path from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles -from starlette.responses import RedirectResponse, JSONResponse +from starlette.responses import RedirectResponse, JSONResponse, HTMLResponse from app.config import settings from urllib.parse import urlencode @@ -55,6 +55,19 @@ async def record_metrics(request: Request, call_next): # Pfade mit eigener Auth, Health-Checks und Favicons: nicht gaten. +_NO_ACCESS_PAGE = """ +Zugang + +
🔒
+

Persönlicher Zugang nötig

+

Bitte öffne die App über deinen persönlichen Zugangslink.

+

Wenn du keinen Link hast, wende dich an deine Betreuungsperson.

+
""" + + _PUBLIC_PREFIXES = ("/api", "/ws", "/health", "/favicon", "/apple-touch-icon") @@ -83,10 +96,9 @@ async def gate_web_ui(request: Request, call_next): token = (_bearer_token(request.headers.get("authorization")) or k or request.cookies.get(CAPABILITY_COOKIE)) if authenticate(request.headers, client_host, token) is None: - login = settings.sso_login_url.strip() - if login: - return RedirectResponse(login, status_code=302) - return JSONResponse({"detail": "Authentication required"}, status_code=401) + # Kein gültiger Zugang -> freundliche Hinweisseite (KEIN SSO-Redirect, der + # nach dem nginx-Cutover in eine Schleife liefe). Senioren nutzen ihren Link. + return HTMLResponse(_NO_ACCESS_PAGE, status_code=401) return await call_next(request) diff --git a/tests/test_forward_auth.py b/tests/test_forward_auth.py index d83f168..34ad5b0 100644 --- a/tests/test_forward_auth.py +++ b/tests/test_forward_auth.py @@ -88,15 +88,15 @@ def test_static_index_gated_without_auth(monkeypatch): assert r.status_code == 401 -def test_static_index_redirects_to_sso(monkeypatch): - # Auth an, keine Identitaet, Login-URL gesetzt -> Redirect zum SSO-Portal. +def test_static_index_shows_no_access_page(monkeypatch): + # Auth an, keine Identitaet, kein Zugangslink -> freundliche Hinweisseite (401), + # KEIN SSO-Redirect mehr (würde nach dem nginx-Cutover in eine Schleife laufen). monkeypatch.setattr(settings, "auth_enabled", True) monkeypatch.setattr(settings, "trusted_auth_header", "") monkeypatch.setattr(settings, "trusted_auth_cookie", "") - monkeypatch.setattr(settings, "sso_login_url", "https://linix.de/yunohost/sso/") r = client.get("/", follow_redirects=False) - assert r.status_code == 302 - assert r.headers["location"].startswith("https://linix.de/yunohost/sso/") + assert r.status_code == 401 + assert "Persönlicher Zugang" in r.text def test_health_open_without_auth(monkeypatch):