Hallo Zusammen,
Ich habe eine Erweiterung vorbereitet, welche das `allowed` Modul um eine optionale Header-basierte Authentifizierung für FHEMWEB erweitert.
Motivation
In Reverse-Proxy-Setups mit vorgelagerter Authentifizierung, z.B. über nginx, Apache, Traefik ForwardAuth, Caddy, oauth2-proxy oder einen dedizierten Auth-Server wie Authelia, steht die Identität des Benutzers oft bereits in HTTP-Headern zur Verfügung. Solche Komponenten können z.B. per Forward-Auth entscheiden, ob ein Request zugelassen wird, und anschließend Benutzer- und Gruppeninformationen in Headern an das Backend weiterreichen.
Bisher kann `allowed` in FHEMWEB praktisch nur `basicAuth` direkt auswerten. Das ist für Direktzugriffe weiterhin sinnvoll, für Proxy-basierte Setups aber etwas unflexibel.
Die Erweitgerung ergänzt deshalb ein neues Attribut `headerAuthPolicy`, mit dem sich eine Header-Policy als JSON formulieren lässt. Unterstützt werden verschachtelte `AND`/`OR`-Gruppen und einfache Matcher wie `present`, `equals`, `notEquals`, `regex`, `contains`, `prefix` und `suffix`.
Beispiel
attr allowedWEB headerAuthPolicy {"op":"AND","items":[{"header":"X-Forwarded-User","match":"present"},{"op":"OR","items":[{"header":"X-Role","match":"equals","value":"fhem-admin"},{"header":"X-Forwarded-Groups","match":"contains","value":"admins"}]}]}
Schematische Darstellung
Browser
|
v
Reverse Proxy / Auth Layer
|
|-- z.B. nginx, Apache, Traefik, Caddy
|-- optional mit vorgelagertem Auth-Server
| z.B. Authelia per Forward-Auth
|-- authentifiziert Benutzer
|-- setzt Header wie:
| X-Forwarded-User: alice
| X-Forwarded-Groups: admins,users
| X-Role: fhem-admin
|
v
FHEMWEB
|
v
allowed
|
|-- prüft noCheckFor
|-- prüft headerAuthPolicy
|-- optional Fallback auf basicAuth
|
v
Zugriff erlaubt / verweigert
Typische Szenarien
- nginx mit auth_request
- Apache mit vorgelagerter Authentifizierung
- Traefik mit ForwardAuth
- Caddy mit vorgelagerter Authentifizierung oder Header-Weitergabe
- Authelia als externer Auth-Server, z.B. per Forward-Auth vor FHEM
- oauth2-proxy
Warum die Erweiterung aus meiner Sicht sinnvoll ist
1. FHEM kann damit sauber hinter einem Reverse Proxy betrieben werden, ohne zusätzlich Browser-Basic-Auth erzwingen zu müssen.
2. Die Authentifizierungsentscheidung bleibt zentral im `allowed`-Modul, also dort, wo heute bereits `basicAuth`, `password`, `validFor` und die Frontend-Zuordnung liegen.
3. Benutzer sind nicht geneigt, die Authentifizierung in FHEM völlig zu deaktivieren, wenn der Proxy die Authentifizierung bereits übernimmt.
4. Die Funktion kann auch bei einer Zertifikats basierten Authentifizierung eingesetzt werden.
5. Mehrere Header und kombinierte Bedingungen lassen sich explizit und validierbar beschreiben, statt einzelne Sonderfälle in Perl-Ausdrücken nachzubauen.
6. Die Policy wird bereits beim Setzen des Attributs validiert. Fehlerhafte JSON- oder Regex-Angaben fallen also sofort auf und nicht erst zur Laufzeit.
7. API Zugriffe könnten in Zukunft ebenso davon profitieren.
Implementierungsszenario
Die eigentliche Policy-Auswertung liegt in einem kleinen Core-Modul `FHEM::Core::Authentication::HeaderPolicy`. `96_allowed.pm` nutzt dieses Modul dann als Integrationsschicht:
- Attribut `headerAuthPolicy` lesen und validieren
- bei FHEMWEB-Requests die Header gegen die Policy prüfen
- bei Erfolg authentifizieren
- bei Nicht-Match optional auf bestehendes `basicAuth` zurückfallen
Wichtig zur Rückwärtskompatibilität
Die bestehende `basicAuth`-Funktion wird nicht verändert. Der Patch ist bewusst additiv:
- Ohne `headerAuthPolicy` verhält sich `allowed` exakt wie bisher.
- Wenn nur `headerAuthPolicy` gesetzt ist, erfolgt die Authentifizierung ausschließlich darüber.
- Wenn `headerAuthPolicy` und `basicAuth` gesetzt sind, dann gilt:
- Header-Match erfolgreich: Zugriff erlaubt
- Header-Match nicht erfolgreich: der bisherige `basicAuth`-Pfad bleibt unverändert als Fallback aktiv
Damit bleiben bestehende Installationen unverändert, und Proxy-basierte Installationen bekommen einen zusätzlichen sauberen Authentifizierungsweg.
Verhalten bei Fehlern
Wenn nur `headerAuthPolicy` gesetzt ist und die Header nicht passen, wird der Zugriff mit `403 Forbidden` abgelehnt. Es wird dann bewusst kein `WWW-Authenticate: Basic` ausgelöst, weil in diesem Modus kein Browser-Basic-Auth-Dialog gewünscht ist.
Tests
Ich habe dazu auch bereits begonnen Unit-Tests für die Policy-Auswertung als auch FHEMWEB-Integrationstests zu erstellen:
- bestehendes `basicAuth` bleibt grün
- Header-Auth Erfolg
- Header-Auth Misserfolg mit `403 Forbidden`
- Fallback auf `basicAuth`, wenn beide Mechanismen konfiguriert sind
- Zusammenspiel mit `noCheckFor`
- Validierung ungültiger Policy beim Setzen des Attributs
Bevor ich den Patch und das Core Modul bereitstelle, gibt es noch Anforderungen die ich berücksichtigen soll/muss?
Grüße Sidey
ZitatIch habe eine Erweiterung vorbereitet, welche das `allowed` Modul um eine optionale Header-basierte Authentifizierung für FHEMWEB erweitert.
Waere es nicht besser ein eigenes Modul dafuer zu bauen, die eine $hash->{AuthorizeFn} implementiert?
Die Authorize() Funktion (was FHEMWEB, etc verwendet) "addiert" die Rueckgabewerte aller solcher Funktionen:
($r == 0 ? "dont care" : $r == 1 ? "allowed" : "prohibited");
Sorry, den Namen verwechselt. es geht hier um Authentifizierung, ergo $hash->{AuthenticateFn}.
Aber auch hier gilt, die Authenticate Funktion addiert die Rueckgabewerte:
Zitat#####################################
# Return
# - 0 for authentication not needed
# - 1 for auth-ok
# - 2 for wrong username/password
# - 3 authentication not needed this time (FHEMWEB special)
3 hat was mit dem noCheckFor Attribut zu tun (https://forum.fhem.de/index.php?topic=141561)
Hallo Rudi,
`$hash->{AuthenticateFn}` hatte ich nicht so richtig auf dem Schirm.
Vielen Dank für diesen Hinweis.
Ich habe über den Vorschlag nachgedacht und denke, ein eigenes Modul wäre hier durchaus möglich.
Eventuell implementiert auch noch mal jemand (oder ich) OIDC.
Ich werde jetzt sowas wie `98_headerAuth.pm` erstellen.
Ich fokussiere mich also auf Authentifizierung.
Das scheint mir die saubere Lösung zu sein und die Autorisierung in allowed zu belassen.
Grüße Sidey