First Upload

This commit is contained in:
friloo 2026-07-01 18:26:57 +02:00 committed by GitHub
parent efc6fcbafa
commit c61677d47f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 3921 additions and 0 deletions

276
docs/SSO-PLAN.md Normal file
View file

@ -0,0 +1,276 @@
# Umsetzungsplan: SSO-Anmeldung für die OpenNIT-Vault-Erweiterung
_Status: Konzept (noch nicht implementiert) · Stand 2026-07-01_
Ziel: Der Nutzer installiert die Erweiterung, gibt **nur die Server-URL** ein und **meldet sich an wie an
OpenNIT** (lokaler Login + 2. Faktor / Microsoft 365 / Keycloak). Die Erweiterung erhält daraufhin ihre
Zugangstokens **automatisch** kein manuelles Kopieren mehr. Kurz vor Ablauf fordert die Erweiterung eine
erneute Anmeldung (Re-Auth).
Dieses Dokument ist der **gründliche Umsetzungsplan** kein Code.
---
## 1. Leitidee
**OpenNIT wird zum OAuth-2.0-Autorisierungsserver für seine eigene Erweiterung.**
Die Erweiterung ist ein **öffentlicher OAuth-Client** (kein Client-Secret) und spricht **ausschließlich mit
OpenNIT** nicht direkt mit Microsoft/Keycloak. Wie sich der Nutzer bei OpenNIT anmeldet (lokal + TOTP/
WebAuthn, Azure AD, Keycloak), ist für die Erweiterung **transparent**: Sie nutzt einfach die bestehende
OpenNIT-Login-Seite. Das löst automatisch **alle** Anmeldemethoden mit einem einzigen Extension-Flow.
Verwendeter Standard: **OAuth 2.0 Authorization Code Flow mit PKCE** (RFC 7636), umgesetzt über die
Browser-API **`chrome.identity.launchWebAuthFlow`**.
> Bereits vorhandene Bausteine in OpenNIT, auf denen aufgesetzt wird:
> `src/Auth/OAuthClient.php` (OIDC/PKCE zu Azure/Keycloak), `src/Auth/TokenManager.php`,
> `src/Auth/SessionManager.php`, `src/Controllers/AuthController.php` (lokaler + Azure + Keycloak Login),
> `src/Vault/VaultManager.php` (`unlockOrSetup`, `unlockOrSetupSso`, `storeVmkInSession`,
> `getVmkFromSession`, PIN-Entsperrung), sowie das bestehende Token-Modell in
> `src/Controllers/VaultApiController.php` (`tokenGenerate`, VMK-Wrapping via `deriveKeyFromToken`).
---
## 2. Zwei Token-Typen (statt einem 180-Tage-Token)
| Token | Lebensdauer (Vorschlag) | Speicherort | Zweck |
|-------|-------------------------|-------------|-------|
| **Access-Token** | kurz (1560 Min) | `chrome.storage.session` (flüchtig) | Bearer für alle `/api/vault/extension/*`-Aufrufe |
| **Refresh-Token** | lang (1430 Tage, rotierend) | `chrome.storage.local` | Holt still neue Access-Tokens; wird bei jeder Nutzung **rotiert** |
Vorteile gegenüber dem heutigen manuellen 180-Tage-Token:
- Ein gestohlener **Access-Token** verfällt in Minuten.
- Der **Refresh-Token rotiert** bei jeder Nutzung → **Diebstahl-Erkennung** (Reuse Detection).
- **Kein Copy-&-Paste** eines langlebigen Geheimnisses.
- **Zentraler Widerruf** über die Web-Oberfläche (Sitzungen beenden).
- Kombinierbar mit dem bereits umgesetzten **serverseitigen PIN-Gate** (Defense in Depth).
---
## 3. Der VMK-Kern (wichtigster Design-Punkt)
OpenNIT ver-/entschlüsselt Tresor-Einträge serverseitig mit dem **Vault Master Key (VMK)**. Jeder
Extension-Token muss den VMK also (server-seitig) verfügbar machen. Heute wrappt `tokenGenerate` den VMK
unter einem aus dem Token abgeleiteten Schlüssel.
**Herausforderung:** Beim **stillen Refresh** (ohne Nutzerinteraktion, ohne Passwort) muss der Server den
VMK weiterhin bereitstellen können.
**Lösung VMK „wandert" mit dem Refresh-Token:**
1. **Bei der Erstanmeldung** (Authorization-Code-Grant) ist der Web-Login gerade erfolgt → der VMK liegt in
der Session (lokaler Login entsperrt per Passwort; SSO-Nutzer per `unlockOrSetupSso`). Der Server:
- wrappt den VMK unter einem aus dem **Refresh-Token** abgeleiteten Schlüssel → speichert ihn in der
Refresh-Zeile,
- wrappt den VMK unter einem aus dem **Access-Token** abgeleiteten Schlüssel → Access-Zeile (kurzlebig).
2. **Beim Refresh:** Server entpackt den VMK mit dem Refresh-Schlüssel, **rotiert** (neuer Refresh-Token,
VMK neu gewrappt, alter Token als „rotiert" markiert), gibt einen neuen Access-Token (VMK gewrappt) aus.
Damit ist der **Refresh-Token** faktisch das langlebige VMK-tragende Geheimnis (wie heute der manuelle
Token) aber **kürzerlebig, rotierend, per SSO bezogen und widerrufbar**. Der **Access-Token** ist das
kurzlebige Arbeitspferd.
> Hinweis PIN-Gate: Das serverseitige PIN-Gate (bereits umgesetzt) bleibt orthogonal bestehen auch mit
> gültigem Access-Token liefern die Secret-Endpunkte bei aktivem Tresor-PIN erst nach PIN-Entsperrung.
> Ob das PIN-Gate bei SSO zusätzlich gefordert wird, ist eine Betreiber-Entscheidung (siehe §9).
---
## 4. Ablauf (Sequenz)
```
Erweiterung OpenNIT (AS) IdP (M365/Keycloak/lokal)
│ │ │
│ 1. launchWebAuthFlow(authorize? │ │
│ client_id, redirect_uri, │ │
│ code_challenge, state, scope) │ │
│──────────────────────────────────▶│ │
│ │ 2. Keine Session? → /auth/login │
│ │────────── Login (lokal+2FA/SSO) ───────▶│
│ │◀───────────── authentifiziert ─────────│
│ │ 3. Vault entsperrt? sonst PIN-Prompt │
│ │ 4. Zustimmung („Vault-Zugriff erlauben")│
│◀── 5. Redirect: redirect_uri?code=…&state ─┤ │
│ 6. state prüfen, code extrahieren │ │
│ 7. POST /oauth/token │ │
│ (code, code_verifier) │ │
│──────────────────────────────────▶│ 8. PKCE prüfen, VMK wrappen │
│◀── {access_token, refresh_token, expires_in, refresh_expires_in} ──────────┤
│ 9. Tokens speichern, loslegen │ │
│ … │ │
│ 10. Access-Token abgelaufen → POST /oauth/token (grant=refresh_token) │
│──────────────────────────────────▶│ 11. rotieren, neuen Access ausgeben │
│◀───────────────────────────────────┤ │
```
Silent-Refresh (Schritt 10/11) läuft unsichtbar. Erst wenn der **Refresh-Token abläuft** oder der Refresh
scheitert, startet die Erweiterung erneut `launchWebAuthFlow` bei noch lebender OpenNIT/IdP-Sitzung ist
das ein **stiller Redirect ohne Eingabe**, sonst ein voller Login.
---
## 5. Neue Server-Endpunkte
Alle unter `/api/vault/extension/oauth/`:
| Methode & Pfad | Auth | Zweck |
|----------------|------|-------|
| `GET /authorize` | Web-Session (Login-Redirect) | Zeigt Zustimmung, ggf. PIN-Entsperrung; erzeugt Auth-Code |
| `POST /token` | öffentlich (PKCE) | `grant_type=authorization_code` **oder** `refresh_token` → Access/Refresh |
| `POST /revoke` | Bearer/Refresh | Widerruft einen Refresh-Token (Logout in der Erweiterung) |
| `GET /sessions` (Web) | Web-Session | Liste aktiver Erweiterungs-Sitzungen im Web-Vault |
| `POST /sessions/{id}/revoke` (Web) | Web-Session | Einzelne Sitzung serverseitig beenden |
Bestehende Endpunkte (`/entries`, `/entries/{id}/password`, `/totp`, `/favicon`, `/status`, `/unlock`,
`/lock`) bleiben unverändert sie akzeptieren künftig **Access-Tokens** genauso wie die bisherigen
manuellen Tokens (siehe §8 Kompatibilität).
---
## 6. Datenmodell (neue Migrationen)
**`vault_oauth_auth_codes`** (kurzlebige Autorisierungscodes)
```
code_hash CHAR(64) PK -- sha256(code)
user_id BIGINT
code_challenge VARCHAR(128) -- PKCE (S256)
redirect_uri VARCHAR(255)
scope VARCHAR(255)
expires_at DATETIME -- ~60 Sekunden
used_at DATETIME NULL -- Einmalverwendung
created_at DATETIME
```
**`vault_oauth_refresh_tokens`** (langlebig, VMK-tragend, rotierend)
```
id BIGINT PK
user_id BIGINT
token_hash CHAR(64) -- sha256(refresh_token)
vmk_enc/nonce/tag -- VMK gewrappt unter Refresh-Schlüssel (HKDF)
device_label VARCHAR(120) -- z. B. "Chrome auf Laptop"
rotated_from BIGINT NULL -- Vorgänger (Reuse-Detection-Kette)
revoked TINYINT DEFAULT 0
refresh_expires_at DATETIME -- absolutes Ablaufdatum (1430 Tage)
created_at, last_used_at DATETIME
```
**Access-Tokens:** die vorhandene Tabelle `vault_extension_tokens` weiternutzen (kurzes `expires_at`,
`unlocked_until` für das PIN-Gate). Optional Spalte `refresh_id` (Herkunft) für Bulk-Revoke.
**Client-Registrierung:** ein fester `client_id` für die Erweiterung + erlaubte Redirect-URIs, konfiguriert
in `system_settings` bzw. einer kleinen `vault_oauth_clients`-Tabelle (Admin-GUI, siehe §7).
---
## 7. Admin-Konfiguration (Pflicht laut OpenNIT-Konventionen)
Neue Admin-Seite `/admin/vault/extension` (Layout `layouts/admin`, Capability `manage_vault_*`), nur bei
aktivem Vault-Modul:
- **Extension-Client-ID** (Vorgabe fix) und **erlaubte Redirect-URIs**. Empfehlung: die konkrete
`https://<extension-id>.chromiumapp.org/` **pinnen** (Store-ID bzw. Unpacked-ID), statt Wildcard.
- **Token-Lebensdauern**: Access (1560 Min), Refresh (1430 Tage), Re-Auth-Vorwarnung (Tage).
- **SSO-Login in der Erweiterung**: an/aus; welche Methoden angeboten werden (erbt aus dem Web-Login).
- **PIN-Gate bei SSO**: zusätzlich fordern (Defense in Depth) oder bei erfolgreichem SSO überspringen.
- Übersicht/Widerruf aktiver Sitzungen.
Alle Werte in der DB (`system_settings`), nicht in Config-Dateien (OpenNIT-Regel). Secrets nie ins Audit-Log.
---
## 8. Erweiterungs-Seite (Client)
- Manifest: Berechtigung **`identity`** ergänzen; Redirect-URI ist `chrome.identity.getRedirectURL()`
(`https://<id>.chromiumapp.org/`).
- **Options/Popup:** Button **„Mit OpenNIT anmelden"** (nach Eingabe der Server-URL). Startet
`launchWebAuthFlow({interactive:true})`.
- **Token-Haltung:** Refresh-Token in `chrome.storage.local`, Access-Token + Ablauf in
`chrome.storage.session`.
- **Auto-Refresh:** Der Background-Service-Worker hält den Access-Token frisch; vor jedem API-Call bei
Ablauf still refreshen. Bei `401`/abgelaufenem Refresh → interaktiver Re-Login.
- **Re-Auth-Vorwarnung:** X Tage vor `refresh_expires_at` ein dezenter Hinweis „Bitte neu anmelden".
- **PKCE/State** clientseitig erzeugen (Web Crypto). `state` gegen CSRF prüfen.
- **Logout:** `POST /oauth/revoke` + lokale Tokens löschen.
---
## 9. Sicherheitsdesign & Bedrohungsmodell
**Was SSO verbessert (ggü. manuellem Token):**
- Kein langlebiges Klartext-Geheimnis zum Kopieren.
- Access-Tokens kurzlebig; Refresh-Tokens **rotieren** → Reuse-Detection: Wird ein bereits rotierter
Refresh-Token erneut vorgelegt, wird die **gesamte Kette widerrufen** und Re-Login erzwungen (+ Audit-Alarm).
- Wiederverwendung der **vorhandenen Anmeldung inkl. 2. Faktor** (M365/Keycloak/lokal+TOTP/WebAuthn).
- **Zentraler Widerruf** je Gerät/Sitzung.
**Pflicht-Härtungen im Flow:**
- **PKCE (S256)** öffentlicher Client, kein Secret.
- **Redirect-URI-Allowlist** (Extension-ID pinnen).
- **`state`** gegen CSRF; **Auth-Code** einmalig, ~60 s gültig, an PKCE-Challenge + redirect_uri + user gebunden.
- **Rate-Limiting** auf `/authorize`, `/token` (bestehender RateLimiter greift; zusätzlich pro Nutzer/Client).
- **Audit-Log** für Ausgabe/Refresh/Rotation/Reuse/Revoke.
- **Nur HTTPS** (bereits in der Erweiterung erzwungen).
**Was bestehen bleibt (bewusst):**
- Der **Refresh-Token ist VMK-tragend „at rest"** in `chrome.storage.local` wie heute der Token.
Restrisiko gemindert durch: Rotation, kürzere Lebensdauer, **PIN-Gate**, optionales **Geräte-Binding**
(client-generierte Device-ID als zusätzlicher Faktor beim Refresh), und schnellen Widerruf.
- `chrome.storage` ist nicht hardware-gebunden ein vollständig kompromittiertes Endgerät bleibt ein
vollständig kompromittiertes Endgerät (gilt für jeden Passwort-Manager).
- Der Server kann prinzipbedingt den VMK entpacken (nötig für serverseitige Krypto). DB-Zugriff = Vollzugriff
(unverändert; separat durch DB-/Server-Härtung zu adressieren).
---
## 10. Kompatibilität & Migration
- **Manuelle Tokens bleiben gültig** (Tabelle `vault_extension_tokens`) kein Bruch bestehender
Installationen. Der SSO-Flow ist **additiv**.
- Die Secret-Endpunkte akzeptieren Access-Tokens **und** Alt-Tokens (dieselbe `authenticateByToken`-Logik,
ergänzt um Access-Token-Lookup).
- Options-Seite: **„Mit OpenNIT anmelden"** wird der Standardweg; **manueller Token** wandert unter
„Erweitert" (für Umgebungen ohne interaktiven Login, z. B. Kiosk/Headless).
- Empfehlung: nach Einführung die Standard-Laufzeit manueller Tokens verkürzen.
---
## 11. Phasenplan & Aufwand (grob)
| Phase | Inhalt | Aufwand |
|-------|--------|---------|
| **0. Feinkonzept** | Endpunkt-/DB-Spezifikation, Admin-Settings festzurren, Lebensdauern | 0,51 Tag |
| **1. Server-AS** | `authorize`/`token`/`revoke`, Migrationen, VMK-Wrapping+Rotation, Reuse-Detection, Audit | 35 Tage |
| **2. Admin-GUI** | Client-/Redirect-Config, Lebensdauern, Sitzungsübersicht/Widerruf | 12 Tage |
| **3. Erweiterung** | `identity`-Flow, Token-Haltung, Auto-Refresh, Re-Auth-UX, Logout | 23 Tage |
| **4. Härtung & Test** | Rate-Limits, Edge-Cases (PIN-gesperrter Vault beim authorize, SSO-only-Nutzer), End-to-End-Tests | 12 Tage |
| **5. Doku & Rollout** | Handbuch, Store-Update, Deprecation-Hinweis manueller Token | 0,51 Tag |
**Gesamt: ~1,52,5 Wochen** für eine solide erste Version. Da OAuth/PKCE-Infrastruktur (`OAuthClient`,
`TokenManager`) und der Vault-Unlock (`unlockOrSetupSso`) bereits existieren, ist ein Teil der Grundlage da.
---
## 12. Edge-Cases, die das Feinkonzept klären muss
- **PIN-gesperrter Vault beim `authorize`**: Ist der Web-Vault gerade PIN-gesperrt, liegt der VMK nicht in
der Session → im Consent-Schritt PIN-Entsperrung verlangen (vorhandener PIN-Flow).
- **SSO-only-Nutzer** (kein lokales Passwort): VMK-Bereitstellung über `unlockOrSetupSso` am `authorize`
bereits gegeben, da der Login gerade lief.
- **Team-Schlüssel**: `provisionPendingTeamKeys` beim Token-Issuing berücksichtigen (wie heute).
- **Mehrere Geräte**: pro Gerät eine Refresh-Kette (`device_label`), unabhängig widerrufbar.
- **Passwortänderung / VMK-Rotation** serverseitig: bestehende Refresh-Tokens ggf. invalidieren → Re-Login.
- **Uhrzeit/Ablauf**: absolute Ablaufzeiten serverseitig führend.
---
## 13. Offene Entscheidungen (für dich)
1. **Lebensdauern**: Access-Token (15/30/60 Min?) und Refresh-Token (14/30 Tage?) + Re-Auth-Vorwarnung (Tage?).
2. **Manuellen Token behalten** (als „Erweitert"-Fallback) oder mittelfristig entfernen?
3. **PIN-Gate bei SSO**: zusätzlich fordern (max. Sicherheit) oder nach erfolgreichem SSO überspringen (Komfort)?
4. **Redirect-URI**: Extension-ID pinnen (empfohlen) vs. `*.chromiumapp.org` erlauben?
5. **Geräte-Binding** des Refresh-Tokens umsetzen (empfohlen) ja/nein?
6. **Angebotene Login-Methoden** in der Erweiterung: alle aus dem Web-Login (lokal+2FA / Azure / Keycloak)?
> Sobald diese sechs Punkte entschieden sind, kann Phase 0 (Feinkonzept mit exakten Endpunkt- und
> DB-Spezifikationen) beginnen.