M0: Skelett + Auth-Grundgerüst (Keycloak-OIDC + API-Token)

- Symfony 7.4 LTS, PHP 8.4, FrankenPHP, SQLite (3-Layer src/UI|Logic|Data)
- Auth: manueller OidcClient (Authorization-Code) im UI-Layer, kein
  Security-Bundle/league; Session-Gate via SessionAuthSubscriber
- API-Guard via statischem X-Api-Token (ApiTokenSubscriber); ExceptionSubscriber
- UserEntity (Auto-Provision) + Init-Migration
- Docker (php:8.4-frankenphp) + compose (essen.ljh31.de, traefik-net external)
- SmokeTests (7) grün: Redirect-Fälle, API-Token, Auth-Render, Logout

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 22:48:24 +02:00
co-authored by Claude Opus 4.8
parent 3c0bee5826
commit cb3cbfb394
56 changed files with 9596 additions and 19 deletions
+29 -14
View File
@@ -1,20 +1,35 @@
# 003 – Single-User, keine Authentifizierung
# 003 – Auth: Keycloak-OIDC (Web) + statischer API-Token (HA)
**Status:** accepted
**Status:** accepted (überarbeitet 2026-09-13; ersetzt „kein Login")
**Kontext.** Das Werkzeug wird im privaten Homelab von **einer** Person (bzw.
Familie via LAN) genutzt. Ein Login-Flow wird zur Barriere, nicht zum Feature.
Später soll Home-Assistant die API konsumieren, und dort ist es einfacher,
einen Shared-Token zu haben statt ein OIDC-Realm zu betreiben.
Familie via LAN) genutzt. Es gibt bereits eine Keycloak-Instanz
(`https://iam.ljh31.de`, Realm `family`), die von `open-webui`/`webuix` genutzt
wird – eine Wiederverwendung ist kostenlos. Home-Assistant soll zusätzlich die
API (`GET /api/today`) konsumieren; ein zweiter, stateless Auth-Mechanismus ist
für Consumer wie HA robuster als eine Browser-Session.
**Entscheidung.** Im MVP **kein Login**. Die API ist hinter dem bestehenden
Traefik-Routing `essen.ljh31.de` (ACME/TLS) erreichbar; wer die Domain kennt,
kann aufrufen. Optional (Phase 2): **Shared-API-Token** per Header
(`X-Api-Token`) für externe Clients wie HA.
**Entscheidung.**
1. **Web-UI:** Keycloak-OIDC (Authorization-Code) gegen Realm `family`,
eigener Client `essens` (Redirect `https://essen.ljh31.de/auth/callback`).
Verifiziert wird das Id-Token (RS256 + JWKS); pro Login wird eine lokale
User-Zeile auto-provisioniert (`essens_user`, gekoppelt an `sub`).
Die App selbst ist ein einfaches **Session-Auth-Gate**
(`SessionAuthSubscriber`): Session-Key `auth_sub` fehlt → Redirect `/login`.
Kein Symfony-Security-Authenticator, keine Rollen.
2. **API:** optionaler statischer Token per Header `X-Api-Token`
(env `API_TOKEN`), geprüft durch `ApiTokenSubscriber` (nur `/api/*`).
Token leer/leer-String → API blockiert; HA bekommt den Token direkt
(kein Keycloak-Refresh-Flow im Consumer).
**Konsequenzen.**
- + Einfachster möglicher Pfad, kein Auth-State, kein JWT.
- + HA-Integration ist trivial (einfacher Token statt OIDC).
- − Kein Benutzer-Abgrenzen, keine Rechte. Im aktuellen Kontext kein Risiko
(LAN + eigene Domain).
- − Wenn es irgendwann öffentliches Hosting wird, ADR-003 ersetzen.
- + Wiederverwendung vorhandener IdP, keine zweite Auth-Welt.
- + HA-Integration bleibt trivial (ein Header statt OIDC im Sensor).
- − Manueller OIDC-Client (JWKS/Id-Token) statt Framework-Lib – bewusst:
kein `league/oauth2-client`/Security-Bundle im Stack, Code bleibt klein
und auditierbar.
- − Zwei Auth-Pfade (Session + Token) – klar getrennt nach Pfad-/Header-Gate.
**Abgelöst.** Fassung „Single-User, keine Authentifizierung" (M0-Start) –
Login via eigener Domain ohne Identität war mit HA-Kopplung und
Familien-Nutzung nicht mehr akzeptabel.
+1 -1
View File
@@ -17,7 +17,7 @@ Wenn sich etwas ändert: **neue** ADR, alte als `superseded` markieren.
|----|-------|--------|
| [001](001-backend-symfony.md) | Backend auf Symfony 7 (PHP) | accepted |
| [002](002-frontend-htmx.md) | Frontend: serverseitig + htmx (kein React) | accepted |
| [003](003-single-user-mit-token.md) | Single-User, keine Authentifizierung (+ optionales API-Token) | accepted |
| [003](003-single-user-mit-token.md) | Auth: Keycloak-OIDC (Web) + statischer API-Token (HA) | accepted |
| [004](004-ha-consumer.md) | Home-Assistant als Consumer, nicht als Host | accepted |
| [005](005-sqlite.md) | SQLite als Datenbank (vorerst) | accepted |
| [006](006-inspiration-algorithmus.md) | Inspiration-Algorithmus: gewichteter Mix | proposed |