# loginz — Integrationsanleitung

Diese Anleitung richtet sich an Entwickler **und KI-Assistenten**, die eine App an loginz anbinden sollen. loginz ist der firmeninterne Single-Sign-on-Dienst: ein standardkonformer **OpenID-Connect-Provider (OIDC)**, der Logins per Zulip-DM (Magic Link + PIN) bestätigt. Deine App muss nichts über Zulip wissen — sie spricht ausschließlich Standard-OIDC (Authorization Code Flow).

## Eckdaten

| | |
| --- | --- |
| Issuer | `https://loginz.tradeo.dev` |
| Discovery | `https://loginz.tradeo.dev/.well-known/openid-configuration` |
| Flow | Authorization Code (`response_type=code`) |
| Scopes | `openid profile email` |
| Client-Authentifizierung | `client_secret_basic` (Default) oder `client_secret_post` |
| PKCE | unterstützt (S256); Pflicht nur für Public Clients ohne Secret |
| Logout | `end_session_endpoint` laut Discovery (RP-initiated logout) |

**Claims, die deine App erhält** (per ID-Token und `userinfo_endpoint`):

| Claim | Bedeutung |
| --- | --- |
| `sub` | Stabile User-ID, Format `zulip:<zulip_user_id>`. **Daran appseitige Daten aufhängen**, nie an der E-Mail. |
| `name` | Voller Name aus Zulip |
| `email` | E-Mail-Adresse aus Zulip |
| `email_verified` | immer `true` |

Es gibt **keinen Consent-Screen** (alle Clients sind firmenintern) und Sessions beim Provider halten 14 Tage — Nutzer, die kürzlich eingeloggt waren, werden ohne erneute Zulip-DM direkt zur App zurückgeleitet.

## Schritt 1: Client registrieren (macht der loginz-Admin)

Deine App braucht drei Werte: `client_id`, `client_secret` und ihre **Redirect-URI** (die Callback-URL deiner App, z. B. `https://meineapp.tradeo.dev/auth/callback`). Gib dem loginz-Admin die Redirect-URI; er trägt in der `clients.json` von loginz ein:

```json
{
  "client_id": "meineapp",
  "client_secret": "<openssl rand -hex 32>",
  "client_name": "Meine App",
  "grant_types": ["authorization_code"],
  "redirect_uris": ["https://meineapp.tradeo.dev/auth/callback"]
}
```

`client_name` erscheint auf der Login-Seite und in der Zulip-DM („Weiter zu Meine App"). Nach dem Eintrag wird loginz neu gestartet.

## Schritt 2: App-Seite einbauen

### Variante A — Fertigsoftware (nur Konfiguration)

- **Gitea:** Admin → Authentifizierungsquellen → Neu → OAuth2, Provider „OpenID Connect", Auto-Discovery-URL = Discovery-URL oben, Client-ID/-Secret eintragen.
- **Grafana** (`grafana.ini`):
  ```ini
  [auth.generic_oauth]
  enabled = true
  name = loginz
  client_id = meineapp
  client_secret = ...
  scopes = openid profile email
  auth_url = https://loginz.tradeo.dev/auth
  token_url = https://loginz.tradeo.dev/token
  api_url = https://loginz.tradeo.dev/me
  ```
- **Nextcloud:** App „OpenID Connect user backend", Discovery-URL + Client-ID/-Secret.
- **Auth.js / NextAuth (Next.js):** Custom-Provider mit
  ```js
  { id: "loginz", name: "loginz", type: "oidc",
    issuer: "https://loginz.tradeo.dev",
    clientId: process.env.LOGINZ_CLIENT_ID,
    clientSecret: process.env.LOGINZ_CLIENT_SECRET }
  ```
- **Node mit Library:** `openid-client` (v6): `client.discovery(new URL('https://loginz.tradeo.dev'), clientId, clientSecret)` — die Library liest die Discovery selbst und übernimmt den ganzen Flow.

### Variante B — selbst implementieren (ohne Library, ~40 Zeilen)

Der Flow im Detail; `AUTH`, `TOKEN`, `USERINFO` stammen aus dem Discovery-Dokument (`authorization_endpoint`, `token_endpoint`, `userinfo_endpoint`):

1. **Login starten** — Browser umleiten auf:
   ```
   {AUTH}?client_id={CLIENT_ID}
     &redirect_uri={REDIRECT_URI}       (URL-encodiert, exakt wie registriert)
     &response_type=code
     &scope=openid%20profile%20email
     &state={ZUFALLSWERT}               (in der Session merken)
   ```
2. **Callback** (`GET {REDIRECT_URI}?code=...&state=...`): `state` gegen die Session prüfen, dann den Code eintauschen:
   ```
   POST {TOKEN}
   Authorization: Basic base64(CLIENT_ID:CLIENT_SECRET)
   Content-Type: application/x-www-form-urlencoded

   grant_type=authorization_code&code={code}&redirect_uri={REDIRECT_URI}
   ```
   Antwort: `{ access_token, id_token, expires_in, ... }`.
3. **Userdaten holen:** `GET {USERINFO}` mit `Authorization: Bearer {access_token}` → `{ sub, name, email, email_verified }`. (Alternativ das `id_token` validieren — Signatur-Keys liegen unter `jwks_uri` aus der Discovery; bei serverseitigem Austausch über TLS ist der Userinfo-Weg ausreichend und einfacher.)
4. **Eigene Session setzen** (Cookie o. Ä.) mit `sub` als User-Schlüssel. Ab hier ist loginz nicht mehr beteiligt — deine App verwaltet ihre Session selbst.

Ein lauffähiges Minimalbeispiel dieses Ablaufs: `examples/demo-client.js` im loginz-Repository.

## Häufige Fehler

- **`redirect_uri` stimmt nicht exakt überein** (Schema, Host, Pfad, Trailing-Slash) → loginz lehnt den Request ab. Muss zeichengenau der registrierten URI entsprechen.
- `state` nicht geprüft → CSRF-Lücke. Immer generieren und im Callback vergleichen.
- Daten an `email` statt `sub` aufgehängt → bricht, wenn sich die Adresse in Zulip ändert.
- `http://`-Redirect-URIs sind nur für `localhost` (Entwicklung) okay, sonst `https://`.
- Discovery-Dokument hart kopieren statt live abrufen → unnötig; die URL ist stabil, das Dokument kann sich weiterentwickeln.

## Checkliste

1. Redirect-URI festlegen und vom Admin in `clients.json` eintragen lassen; `client_id` + `client_secret` erhalten.
2. Secret als Umgebungsvariable in die App (nie ins Repo).
3. OIDC-Client konfigurieren (Variante A) oder Flow einbauen (Variante B).
4. Testen: Login-Button → loginz-Seite erscheint → Name eingeben → Zulip-DM → PIN/Link → zurück in der App, `sub`/`name`/`email` vorhanden.
