Files
Laufzettel/docs/betrieb/authentik.md
T
Florian WildeandClaude Opus 5 49ce06f206
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat: Projektgerüst, gehärteten Betriebsstand und Prozess aufsetzen
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>
2026-09-24 23:33:49 +02:00

4.5 KiB

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:
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:

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:

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:

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