feat(auth): Client-Secret in den Einstellungen pflegen, Warnung vor Ablauf

Das Secret der App-Registrierung laeuft in Entra ab. Bisher hiess das: jemand
mit SSH-Zugang aendert die .env und startet den Dienst neu - und niemand sieht
vorher, wann es soweit ist. Laeuft es aus, stehen Anmeldung UND
Postfachabgleich still, denn beide haengen an derselben Registrierung.

Neu unter Einstellungen > Anmeldung: Secret eintragen, Ablaufdatum hinterlegen,
Restlaufzeit ablesen. Ab 30 Tagen vor dem Ablauf bekommen Administratoren ueber
jeder Ansicht ein Hinweisband, das direkt dorthin fuehrt; nach dem Ablauf wird
es rot. Der Hinweis reist mit /api/badges mit, das die App ohnehin bei jedem
Laden abruft.

Bewusst so entschieden:
  - Das Secret muss dafuer in die Datenbank, entgegen der bisherigen Regel
    "keine Geheimnisse in crm_settings". Es liegt deshalb AES-256-GCM-
    verschluesselt dort; der Schluessel kommt aus CRM_ENCRYPTION_KEY, ersatz-
    weise JWT_SECRET, und steht damit nie im selben Dump.
  - Vor dem Speichern holt das CRM mit dem eingegebenen Wert ein Token bei
    Microsoft. Ein Tippfehler wird gar nicht erst uebernommen.
  - Gelesen wird Datenbank vor .env; der .env-Wert bleibt Rueckfallebene, und
    "Hinterlegtes Secret verwerfen" schaltet dorthin zurueck.
  - Passt der Schluessel nicht mehr, meldet die Oberflaeche das ausdruecklich,
    statt still auf die .env zurueckzufallen - sonst sucht man den Fehler an
    der falschen Stelle.
  - Das Ablaufdatum wird von Hand eingetragen. Aus Entra lesen braeuchte
    Application.Read.All, die die App bewusst nicht hat.

MAIL_CONFIGURED und SSO_CONFIGURED sind dadurch von Konstanten zu den
Funktionen mailConfigured() und ssoConfigured() geworden - die Pruefung muss
jetzt in die Datenbank sehen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-09-21 08:38:28 +02:00
co-authored by Claude Opus 5
parent e271f5c1e8
commit c8eaaa1947
22 changed files with 937 additions and 64 deletions
+77 -3
View File
@@ -100,11 +100,78 @@ antwortet dann mit 404, das ist eingeplant).
| `GET` | `/api/auth/sso/start` | offen | Umleitung zu Entra |
| `GET` | `/api/auth/sso/callback` | offen | Rückleitung von Entra, endet in `/?sso_ticket=...` |
| `POST` | `/api/auth/sso/ticket` | offen | Ticket gegen CRM-Token tauschen |
| `GET` | `/api/auth/sso/config` | admin | Status, fehlende Variablen, Umleitungsadresse |
| `GET` | `/api/auth/sso/config` | admin | Status, fehlende Variablen, Umleitungsadresse, Zustand des Secrets |
| `PUT` | `/api/auth/microsoft-secret` | admin | Secret und/oder Ablaufdatum setzen |
| `DELETE` | `/api/auth/microsoft-secret` | admin | hinterlegtes Secret verwerfen, `.env` gilt wieder |
Scheitert etwas, landet der Browser auf `/?sso_error=<Meldung>`; die
Anmeldemaske zeigt den Text im gewohnten roten Kasten.
## Client-Secret pflegen
Das Secret der App-Registrierung läuft ab - in Entra sind 6 oder 24 Monate
üblich. Läuft es aus, stehen **Anmeldung und Postfachabgleich gleichzeitig**
still, denn beide hängen an derselben Registrierung. Damit dafür niemand auf den
Server muss, lässt es sich unter *Einstellungen → Anmeldung* eintragen.
```mermaid
flowchart LR
A["Einstellungen →<br/>Anmeldung"] -->|"PUT /microsoft-secret"| B["Probe bei Microsoft:<br/>Client-Credentials-Token"]
B -->|abgelehnt| C["nichts gespeichert,<br/>Meldung von Entra"]
B -->|angenommen| D["AES-256-GCM<br/>verschlüsseln"]
D --> E[("crm_settings")]
E --> F["Graph-Token verwerfen -<br/>der Zwischenspeicher<br/>gehört zum alten Secret"]
```
**Reihenfolge beim Lesen:** erst `crm_settings`, dann `MS_CLIENT_SECRET` aus der
`.env`. Wer nichts einträgt, merkt von der Neuerung nichts; der `.env`-Wert
bleibt die Rückfallebene. *Hinterlegtes Secret verwerfen* schaltet bewusst
dorthin zurück.
**Warum verschlüsselt:** ein Geheimnis im Klartext in der Datenbank stünde in
jedem Dump. Der Schlüssel kommt aus `CRM_ENCRYPTION_KEY`, ersatzweise aus
`JWT_SECRET` - beide stehen in der `.env` und damit nie im selben Dump. Wird
eine dieser Variablen gewechselt, lässt sich das gespeicherte Secret nicht mehr
lesen; die Oberfläche sagt das dann ausdrücklich, und es wird einmal neu
eingetragen. Ein stiller Rückfall auf die `.env` findet in diesem Fall **nicht**
statt - sonst sucht man den Fehler an der falschen Stelle.
**Probe vor dem Speichern:** das CRM holt mit dem eingegebenen Wert ein
Client-Credentials-Token. Erst wenn Microsoft es ausstellt, wird gespeichert. Ein
Tippfehler legt so nichts lahm.
### Warnung vor dem Ablauf
Das Ablaufdatum wird von Hand eingetragen - es aus Entra zu lesen bräuchte die
Berechtigung `Application.Read.All`, die die App bewusst nicht hat.
Ab **30 Tagen** vor dem Ablauf (`WARN_DAYS` in `msSecret.js`) erscheint für
Administratoren über jeder Ansicht ein Hinweisband, das direkt in diesen
Einstellungsreiter führt; nach dem Ablauf wird es rot. Der Hinweis reist mit
`GET /api/badges` mit, das die App ohnehin bei jedem Laden abruft - ein eigener
Endpunkt wäre eine zweite Anfrage für eine Zahl. Benutzer ohne Adminrolle
bekommen ihn nicht: sie können das Secret nicht erneuern.
| Zustand | Bedeutung |
| --- | --- |
| `ok` | mehr als 30 Tage übrig |
| `warnung` | 0 bis 30 Tage übrig - Hinweisband in Gelb |
| `abgelaufen` | Datum liegt in der Vergangenheit - Hinweisband in Rot |
| `unbekannt` | kein Datum hinterlegt, keine Warnung möglich |
### So wird erneuert
1. Entra → *App-Registrierungen* → **perinco crm** → *Zertifikate & Geheimnisse*
→ **Neuer geheimer Clientschlüssel**.
2. Wert sofort kopieren - Entra zeigt ihn nur einmal.
3. Im CRM unter *Einstellungen → Anmeldung* eintragen, Ablaufdatum übernehmen,
speichern.
4. Erst danach das alte Geheimnis in Entra löschen.
Ein Neustart des Backends ist nicht nötig: das Secret wird bei jedem Bedarf
frisch gelesen, und das zwischengespeicherte Graph-Token wird beim Speichern
verworfen.
## Einrichtung in Entra
Die App-Registrierung **perinco crm** existiert bereits für die
@@ -147,7 +214,9 @@ vertraulicher Client mit Secret.
| `MS_SSO_ENABLED` | ja | nur `true` schaltet die Anmeldung ein |
| `MS_SSO_REDIRECT_URI` | ja | exakt die in Entra eingetragene Adresse |
| `MS_SSO_ALLOWED_DOMAINS` | nein | Positivliste von E-Mail-Domains, leer = keine zusätzliche Prüfung. Im Betrieb: `perinco.de,icon-experts.de` |
| `MS_TENANT_ID`, `MS_CLIENT_ID`, `MS_CLIENT_SECRET` | ja | dieselben Werte wie beim Postfach |
| `MS_TENANT_ID`, `MS_CLIENT_ID` | ja | dieselben Werte wie beim Postfach |
| `MS_CLIENT_SECRET` | nein | Rückfallebene; Vorrang hat das in den Einstellungen hinterlegte Secret |
| `CRM_ENCRYPTION_KEY` | nein | verschlüsselt das hinterlegte Secret; ohne den Wert dient `JWT_SECRET` als Schlüssel |
Nicht jedes CRM-Konto trägt eine `@perinco.de`-Adresse - `icon-experts.de`
gehört dazu. Wer eine Domain vergisst, sperrt die betroffenen Leute vom
@@ -182,6 +251,9 @@ Microsoft-Weg aus; die Anmeldung mit Passwort funktioniert für sie weiterhin.
| „Die Anmeldung ist abgelaufen" | älter als 10 Minuten, oder das Backend wurde zwischendurch neu gestartet (der offene Vorgang liegt im Prozessspeicher) |
| „Anmeldung kommt aus einem fremden Verzeichnis" | Antwort gehört zu einem anderen Tenant - `MS_TENANT_ID` prüfen |
| Profilfoto fehlt | kein Foto in M365, Bild grösser als 200 KB, oder `ProfilePhoto.Read.All` ohne Zustimmung |
| „Microsoft hat dieses Secret abgelehnt" | Wert falsch kopiert oder das Geheimnis in Entra bereits gelöscht - es wurde nichts gespeichert |
| „nicht entschlüsselbar" im Reiter *Anmeldung* | `JWT_SECRET` oder `CRM_ENCRYPTION_KEY` wurde gewechselt; Secret einmal neu eintragen |
| Hinweisband erscheint nicht | kein Ablaufdatum hinterlegt, oder das Konto ist kein Administrator |
## Quellen im Code
@@ -193,5 +265,7 @@ Microsoft-Weg aus; die Anmeldung mit Passwort funktioniert für sie weiterhin.
| `server/src/schema.sql` | Spalte `crm_users.entra_oid` samt Unique-Index |
| `src/lib/auth.tsx` | Ticket einlösen, Adresszeile aufräumen, `ssoError` |
| `src/components/LoginScreen.tsx` | Knopf „Mit Microsoft anmelden" |
| `src/components/settings/SsoSettings.tsx` | Reiter *Anmeldung* in den Einstellungen |
| `src/components/settings/SsoSettings.tsx` | Reiter *Anmeldung* samt Pflege des Secrets |
| `server/src/msSecret.js` | Secret verschlüsseln, Ablauf berechnen, bei Microsoft prüfen |
| `src/App.tsx` | Hinweisband für Administratoren vor dem Ablauf |
| `server/test/sso.test.mjs` | Prüfungen ohne Netz und Datenbank |