Legt die Grundlage, auf der alle weiteren Meilensteine nur noch Fachlichkeit ergänzen: Next 16 mit App Router und TypeScript strict, Tailwind 4, Prisma 7, Vitest 5, Playwright, ESLint, Prettier und die Hooks für Lint und Commits. Zwei Dinge sind bewusst so gebaut und durch Tests festgehalten: Die Datenbank ist im Betriebsstand von außen nicht erreichbar. Sie hat kein ports:-Mapping und hängt allein im Netz "intern", das ohne Gateway läuft; nur "web" kommt zusätzlich nach draußen. tests/betriebsstand.test.ts schlägt an, falls ein Port-Mapping zurückkehrt. Geprüft, nicht behauptet: docker compose up startet den Stack, 5432 ist vom Host aus zu, aus "web" offen. pnpm pruefe:doku hält Dokumentation und Code zusammen — Version im Changelog, Issue-Dateien gegen ihre Übersicht, keine Verweise auf fehlende Dateien, keine fehlenden Bilder. Das Skript wurde gegen vier eingebaute Fehler gegengeprüft. Drei Versions-Obergrenzen sind nötig und in CLAUDE.md begründet: eslint bleibt auf 9.x, weil eslint-plugin-react keinen Nachfolger für eslint 10 hat und das Regelwerk sonst beim Laden abbricht; typescript bleibt auf 6.0.x, weil typescript-eslint TypeScript 7 noch nicht unterstützt; prisma ist exakt auf 7.10.0 gepinnt, weil sein latest-Tag auf einen 8.0-RC zeigt, der von @prisma/client aber nicht. Auth.js entfällt entgegen der ursprünglichen Vorgabe: v5 steht dauerhaft auf Beta, das Projekt ist im Wartungsmodus und sein Prisma-Adapter deckt Prisma 7 nicht ab. Stattdessen openid-client und jose, wie in ProxLedger. Dieser erste Commit geht direkt auf main, weil es noch keinen Branch gibt, von dem abzuzweigen wäre. Ab #006 läuft jedes Issue über einen eigenen Branch. Closes #5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
123 lines
4.5 KiB
Markdown
123 lines
4.5 KiB
Markdown
# Anmeldung über authentik (OIDC)
|
|
|
|
Laufzettel spricht generisches OpenID Connect mit Discovery. Der Anbieter muss also nur einen
|
|
`/.well-known/openid-configuration`-Endpunkt anbieten — authentik tut das. Ein anderer Anbieter
|
|
(Keycloak, Entra ID, Zitadel) funktioniert genauso, nur heißen die Menüs anders.
|
|
|
|
## 1. Provider in authentik anlegen
|
|
|
|
**Anwendungen → Provider → Erstellen → OAuth2/OpenID Provider**
|
|
|
|
| Feld | Wert |
|
|
|---|---|
|
|
| Name | `Laufzettel` |
|
|
| Authorization flow | `default-provider-authorization-explicit-consent` (oder implicit) |
|
|
| Client type | `Confidential` |
|
|
| Redirect URIs | `https://laufzettel.example.de/api/auth/oidc/rueckruf` |
|
|
| Signing Key | ein vorhandener Schlüssel |
|
|
| Scopes | `openid`, `profile`, `email` und ein Scope, der `groups` liefert |
|
|
|
|
Die Redirect-URI ist `APP_URL` plus `/api/auth/oidc/rueckruf` — exakt so, mit demselben Schema
|
|
und ohne abschließenden Schrägstrich. Läuft Laufzettel hinter einem Reverse Proxy, ist `APP_URL`
|
|
die **öffentliche** Adresse, nicht die interne.
|
|
|
|
`Client ID` und `Client Secret` notieren.
|
|
|
|
## 2. Gruppen in den Token bringen
|
|
|
|
Laufzettel liest die Rolle aus dem Claim `groups`. In authentik gibt es dafür den mitgelieferten
|
|
Scope-Mapping `authentik default OAuth Mapping: OpenID 'profile'` nicht — du brauchst ein eigenes
|
|
Mapping:
|
|
|
|
**Anpassung → Property Mappings → Erstellen → Scope Mapping**
|
|
|
|
- Name: `Laufzettel Gruppen`
|
|
- Scope name: `groups`
|
|
- Expression:
|
|
|
|
```python
|
|
return {
|
|
"groups": [gruppe.name for gruppe in request.user.ak_groups.all()],
|
|
}
|
|
```
|
|
|
|
Dieses Mapping beim Provider unter **Advanced protocol settings → Scopes** auswählen.
|
|
|
|
## 3. Anwendung anlegen
|
|
|
|
**Anwendungen → Anwendungen → Erstellen**, den Provider zuordnen, Slug z. B. `laufzettel`.
|
|
Über die Anwendung steuerst du per Policy, wer sich überhaupt anmelden darf.
|
|
|
|
## 4. Gruppen für die Rollen
|
|
|
|
Lege in authentik vier Gruppen an und trage die Mitglieder ein:
|
|
|
|
| Gruppe | Rolle in Laufzettel | Darf |
|
|
|---|---|---|
|
|
| `lz-admin` | `ADMIN` | alles, inkl. Benutzer, Verzeichnis, Abteilungen, Archiv-Verwaltung |
|
|
| `lz-redakteur` | `REDAKTEUR` | Vorlagen anlegen, bearbeiten, veröffentlichen |
|
|
| `lz-techniker` | `TECHNIKER` | Läufe starten, abarbeiten, abschließen, archivieren; Geräte pflegen |
|
|
| `lz-leser` | `LESER` | nur lesen |
|
|
|
|
Ist jemand in mehreren Gruppen, gewinnt die stärkere Rolle.
|
|
|
|
## 5. Laufzettel konfigurieren
|
|
|
|
In der `.env`:
|
|
|
|
```dotenv
|
|
APP_URL=https://laufzettel.example.de
|
|
|
|
OIDC_ISSUER=https://authentik.example.de/application/o/laufzettel/
|
|
OIDC_CLIENT_ID=...
|
|
OIDC_CLIENT_SECRET=...
|
|
OIDC_NAME=authentik
|
|
|
|
OIDC_GROUP_ADMIN=lz-admin
|
|
OIDC_GROUP_EDITOR=lz-redakteur
|
|
OIDC_GROUP_TECHNICIAN=lz-techniker
|
|
OIDC_GROUP_VIEWER=lz-leser
|
|
```
|
|
|
|
Den `OIDC_ISSUER` zeigt authentik beim Provider unter **OpenID Configuration Issuer** an. Der
|
|
abschließende Schrägstrich gehört dazu.
|
|
|
|
Danach neu starten:
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
## 6. Was beim Login passiert
|
|
|
|
- Ein unbekannter Benutzer wird beim ersten Login **angelegt** (JIT-Provisioning)
|
|
- Die Rolle wird bei **jedem** Login aus `groups` neu gesetzt — eine Änderung in authentik greift
|
|
ab der nächsten Anmeldung
|
|
- Ist der Benutzer in keiner der vier Gruppen, bekommt er `LESER`
|
|
- Wer vor dem Login eine bestimmte Seite aufgerufen hat (z. B. `/t/LZ-7K3F9Q` nach dem Scannen
|
|
eines QR-Codes), landet danach genau dort
|
|
|
|
## 7. Notfallzugang
|
|
|
|
Fällt authentik aus, kommt niemand mehr in Laufzettel. Für diesen Fall gibt es den lokalen Login:
|
|
|
|
```dotenv
|
|
AUTH_LOCAL_ENABLED=true
|
|
```
|
|
|
|
Er ist standardmäßig **aus**. Ein lokales Admin-Konto legst du über den Seed an
|
|
(`SEED_ADMIN_EMAIL`, `SEED_ADMIN_PASSWORD`) oder als Admin unter **Admin → Benutzer**. Die
|
|
Passwörter liegen als argon2-Hash in der Datenbank.
|
|
|
|
Empfehlung: ein einziges lokales Admin-Konto, Passwort im Passwortmanager, `AUTH_LOCAL_ENABLED`
|
|
im Normalbetrieb auf `false` und nur im Notfall umgestellt.
|
|
|
|
## 8. Fehlersuche
|
|
|
|
| Symptom | Ursache |
|
|
|---|---|
|
|
| `redirect_uri_mismatch` | Die URI in authentik stimmt nicht zeichengenau mit `APP_URL` + `/api/auth/oidc/rueckruf` überein — oft `http` gegen `https` oder ein abschließender Schrägstrich |
|
|
| Nach dem Login wieder auf der Anmeldeseite | Das Cookie ist `Secure`, die Verbindung aber `http`. Entweder HTTPS nutzen oder zum Testen über `localhost` gehen |
|
|
| Jeder bekommt `LESER` | Der Claim `groups` fehlt im Token. Scope-Mapping aus Schritt 2 angelegt und beim Provider ausgewählt? |
|
|
| Discovery schlägt fehl | `OIDC_ISSUER` falsch oder von der App aus nicht erreichbar. Im Compose-Stand hängt `web` im Netz `extern` — prüfen, ob der Name auflösbar ist |
|