feat(auth): Anmeldung ueber Microsoft Entra (SSO)
OpenID Connect mit Authorization Code Flow und PKCE, vollstaendig
serverseitig - der Browser sieht weder Tenant- noch Client-ID. Genutzt wird
dieselbe App-Registrierung wie die Postfachanbindung, also kein zweites
Secret; delegiert statt app-only.
Nach dem Token-Tausch beweist ein Aufruf von /me bei Graph, dass das
Zugriffstoken gilt, und liefert die Objekt-ID. Sie landet in der neuen Spalte
crm_users.entra_oid und ist danach die stabile Verknuepfung - die E-Mail-
Adresse eines Menschen kann sich aendern, die Objekt-ID nicht.
Bewusst so entschieden:
- Es werden keine Konten angelegt. Wer kein CRM-Konto mit derselben Adresse
hat, wird abgewiesen; Zugriff vergibt weiter die Nutzerverwaltung.
- Die Anmeldung mit Benutzername und Passwort bleibt fuer alle bestehen -
Notzugang, falls Entra oder das Secret ausfaellt.
- Das CRM-Token kommt als Einmal-Ticket zurueck, nicht in der Adresszeile:
die Rueckleitung ist ein GET und stuende sonst in Browserverlauf und
Apache-Log.
- Keine Signaturpruefung des ID-Tokens und damit keine JWKS-Bibliothek. Es
kommt ueber TLS direkt vom Token-Endpunkt (OIDC Core 3.1.3.7); geprueft
werden aud, tid, nonce und exp, und der Graph-Aufruf belegt den Rest.
Ohne MS_SSO_ENABLED=true bleibt alles aus und die Anmeldemaske sieht aus wie
bisher.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2d84bac91d
commit
419e57b55d
+31
-2
@@ -94,7 +94,7 @@ Alle unter `router.use(requireAuth)`.
|
||||
|
||||
| Router | Endpunkte | Rechte |
|
||||
| --- | --- | --- |
|
||||
| `auth.js` | `POST /login`, `GET /me`, `PATCH /me`, `POST /change-password` | `/login` offen, Rest angemeldet |
|
||||
| `auth.js` | `POST /login`, `GET /me`, `PATCH /me`, `POST /change-password`; SSO: `GET /sso/status`, `GET /sso/start`, `GET /sso/callback`, `POST /sso/ticket`, `GET /sso/config` | `/login` und die vier offenen SSO-Endpunkte ohne Anmeldung, `/sso/config` Admin, Rest angemeldet |
|
||||
| `users.js` | `GET /directory`; danach `requireAdmin`: `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | `/directory` für alle, Rest Admin |
|
||||
| `leads.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id`; Vorbereich: `GET /mail-pool/sources`, `POST /mail-pool/promote`, `POST /:id/promote` | jeder Angemeldete |
|
||||
| `connections.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | jeder Angemeldete |
|
||||
@@ -200,6 +200,35 @@ Eigenheiten, die man kennen muss:
|
||||
* Der Lauf ist **einfach gesperrt** (`running` in `sync.js`) - ein zweiter Aufruf
|
||||
kommt sofort mit `skipped` zurück, statt parallel zu laufen.
|
||||
|
||||
### `server/src/sso/`
|
||||
|
||||
Anmeldung über Microsoft Entra, fachlich in
|
||||
[13 Anmeldung über Microsoft](13-anmeldung-sso.md). Dieselbe App-Registrierung
|
||||
wie das Postfach, aber delegiert statt app-only - deshalb ein eigenes Modul und
|
||||
kein Import aus `mail/`: die Anmeldung läuft auch, wenn die Postfachanbindung
|
||||
aus ist.
|
||||
|
||||
| Datei | Inhalt |
|
||||
| --- | --- |
|
||||
| `config.js` | `.env` lesen, `SSO_CONFIGURED`, `missingSsoConfig()`, `domainAllowed()` |
|
||||
| `flow.js` | `beginLogin()` (PKCE, state, nonce), `takePendingLogin()`, `exchangeCode()`, `verifyIdToken()`, `fetchProfile()`, `fetchPhoto()`, `createTicket()` / `redeemTicket()` |
|
||||
|
||||
Eigenheiten, die man kennen muss:
|
||||
|
||||
* Offene Anmeldungen und Tickets liegen **im Prozessspeicher**, nicht in der
|
||||
Datenbank: Lebensdauer 10 Minuten bzw. 60 Sekunden, und das Backend läuft als
|
||||
einzelne Instanz. Ein Neustart verwirft höchstens eine gerade laufende
|
||||
Anmeldung.
|
||||
* `state` und Ticket gelten je genau einmal - beide werden beim Lesen entfernt.
|
||||
* `verifyIdToken()` prüft `aud`, `tid`, `nonce` und `exp`, aber nicht die
|
||||
Signatur. Das Token kommt über TLS direkt vom Token-Endpunkt (OIDC Core
|
||||
3.1.3.7), und der anschliessende `/me`-Aufruf belegt ohnehin, dass das
|
||||
Zugriffstoken gilt. Das spart eine JWKS-Bibliothek.
|
||||
* `fetchPhoto()` schluckt jeden Fehler und gibt `null` zurück - wer kein
|
||||
Profilfoto hat, soll sich trotzdem anmelden können.
|
||||
* `findSsoUser()` in `routes/auth.js` legt **nie** ein Konto an und sucht über
|
||||
die E-Mail-Adresse nur, solange `entra_oid IS NULL` ist.
|
||||
|
||||
### `server/src/routes/scanner.js`
|
||||
|
||||
Die portierte Scanner-API unter `/api/scanner`, vollständig in
|
||||
@@ -294,7 +323,7 @@ Uhrzeit, dieses Jahr Tag und Monat, sonst mit Jahr.
|
||||
|
||||
| Datei | Hält | Eigenheit |
|
||||
| --- | --- | --- |
|
||||
| `auth.tsx` | Benutzer, `isAdmin`, `login`, `logout`, `updateUser` | prüft beim Start das gespeicherte Token gegen `/auth/me` |
|
||||
| `auth.tsx` | Benutzer, `isAdmin`, `login`, `logout`, `updateUser`, `ssoError` | prüft beim Start das gespeicherte Token gegen `/auth/me`; kommt der Browser von Microsoft zurück, hat `?sso_ticket=` Vorrang - es wird eingelöst und die Adresszeile per `history.replaceState()` geleert |
|
||||
| `theme.tsx` | Farbwelt (perinco/Pixel Art/Outdoor/Cyberpunk/Rainy/Snow, Hintergrundgrafiken in `public/themes/`; bei Rainy hängt er zusätzlich `components/RainyWeather.tsx` mit Regen und Wetterleuchten ein, bei Snow `components/SnowWeather.tsx` mit Schneefall, Schneedecke und Räumfahrzeug, bei Aura `components/AuraWeather.tsx` mit Sonnenstrahlen) und Modus (hell/dunkel/System) | Beides gehört dem Benutzer und steht in `crm_users`; `localStorage` ist nur der Zwischenspeicher fürs Inline-Skript in `index.html`. Doppelklick auf den Modus-Schalter folgt wieder dem System |
|
||||
| `navigation.tsx` | `currentView`, `goTo(view, { tab, leadId })` | erspart es, Callbacks durch vier Ebenen zu reichen; `leadId` öffnet den Kontakt direkt |
|
||||
| `whisper.tsx` | Modellstatus, Worker, Fortschritt, `transcribe` | ein Worker für die ganze App |
|
||||
|
||||
Reference in New Issue
Block a user