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
@@ -86,6 +86,12 @@ Anmeldeschirm (`setUnauthorizedHandler` in `src/lib/api.ts`). Das Token wird bei
|
||||
jeder Anfrage neu gegen die Datenbank geprüft - ein deaktivierter Benutzer ist
|
||||
sofort draußen, ohne auf den Ablauf des Tokens zu warten.
|
||||
|
||||
Der zweite Weg zu demselben Token ist die Anmeldung über Microsoft Entra: statt
|
||||
`bcrypt.compare` steht dort der OIDC-Ablauf mit Microsoft, danach geht es
|
||||
identisch weiter - dasselbe JWT, dieselbe Prüfung bei jeder Anfrage. Wer kein
|
||||
CRM-Konto hat, kommt auch mit gültigem Microsoft-Konto nicht hinein.
|
||||
Einzelheiten: [13 Anmeldung über Microsoft](13-anmeldung-sso.md).
|
||||
|
||||
## Rollen
|
||||
|
||||
```mermaid
|
||||
|
||||
@@ -31,6 +31,7 @@ erDiagram
|
||||
int id PK
|
||||
varchar username UK
|
||||
varchar email UK
|
||||
varchar entra_oid UK "Objekt-ID des Microsoft-Kontos, NULL = nie per SSO angemeldet"
|
||||
varchar name
|
||||
enum role "admin | user"
|
||||
varchar color "Terminfarbe, NULL = aus Id abgeleitet"
|
||||
|
||||
+4
-3
@@ -33,8 +33,9 @@ flowchart TD
|
||||
E --> E1["Nutzerverwaltung (admin)"]
|
||||
E --> E2["Mein Konto"]
|
||||
E --> E3["Postfach (admin)"]
|
||||
E --> E4["Scanner (admin)"]
|
||||
E --> E5["Whisper"]
|
||||
E --> E4["Anmeldung (admin)"]
|
||||
E --> E5["Scanner (admin)"]
|
||||
E --> E6["Whisper"]
|
||||
```
|
||||
|
||||
*Postfach* und *Kontakt-Vorschläge* sind Reiter **innerhalb** der Ansicht
|
||||
@@ -127,7 +128,7 @@ src/
|
||||
Vorbereich "Aus Mails" mit Dublettenabgleich
|
||||
mail/ Postfachliste, Detail, Zuordnung, Kontakt-Vorschläge
|
||||
scanner/ Dashboard, Unpassend, Suchagenten, Börsen-Konten
|
||||
settings/ Reiter: Nutzerverwaltung, Konto, Postfach, Scanner, Whisper
|
||||
settings/ Reiter: Nutzerverwaltung, Konto, Postfach, Anmeldung, Scanner, Whisper
|
||||
whisper/ Einrichtung der Spracherkennung
|
||||
ui/ Modal, Avatare, Diktatfeld
|
||||
```
|
||||
|
||||
@@ -63,6 +63,9 @@ mitten im Scrapen beenden.
|
||||
| `MS_MAIL_FOLDER` | Ordner, Standard `inbox` |
|
||||
| `MAIL_SYNC` | Postfachabgleich, Standard an, nur `false` schaltet ab |
|
||||
| `MAIL_MAX_PAGES`, `MAIL_MAX_BODY_CHARS` | Bremsen für Erstabgleich und Textgröße |
|
||||
| `MS_SSO_ENABLED` | Anmeldung über Microsoft, Standard aus, nur `true` schaltet ein |
|
||||
| `MS_SSO_REDIRECT_URI` | Umleitungsadresse, zeichengenau wie in Entra eingetragen |
|
||||
| `MS_SSO_ALLOWED_DOMAINS` | Positivliste von E-Mail-Domains, leer = keine zusätzliche Prüfung |
|
||||
|
||||
Die Scanner-Datenbank wird beim Start automatisch angelegt
|
||||
(`CREATE DATABASE IF NOT EXISTS`), Tabellen kommen aus `sequelize.sync()`, Portale
|
||||
@@ -77,6 +80,10 @@ Exchange Online: [11 Postfachanbindung](11-postfach.md).
|
||||
> `.env` muss der Dienst neu starten, ein Speichern in der Oberfläche genügt
|
||||
> nicht - dort liegen nur Intervall, Schalter und Zeitraum (`crm_settings`).
|
||||
|
||||
Die `MS_SSO_*`-Werte gehören zur Anmeldung über Microsoft und nutzen dieselbe
|
||||
App-Registrierung wie das Postfach. Ohne sie bleibt es bei Benutzername und
|
||||
Passwort. Einrichtung: [13 Anmeldung über Microsoft](13-anmeldung-sso.md).
|
||||
|
||||
## Playwright
|
||||
|
||||
`playwright` ist auf **exakt 1.60.0** gepinnt. Der passende Chromium-Build
|
||||
|
||||
@@ -21,12 +21,20 @@ Ein CRM für die Projektvermittlung. Es beantwortet vier Fragen:
|
||||
`https://crm.perinco.de` (alternativ `https://pcrm.perinco.de`). Anmelden geht mit **Benutzername oder E-Mail** plus
|
||||
Passwort. Die Sitzung hält 12 Stunden, danach fragt die Anwendung erneut.
|
||||
|
||||
Steht auf der Maske **Mit Microsoft anmelden**, geht es auch ohne zweites
|
||||
Passwort: ein Klick, die gewohnte Microsoft-Anmeldung, zurück im CRM. Das setzt
|
||||
voraus, dass es hier schon ein Konto mit derselben E-Mail-Adresse gibt - sonst
|
||||
kommt der Hinweis, sich an einen Administrator zu wenden. Wer noch kein
|
||||
Profilfoto im CRM hat, bekommt bei der ersten Anmeldung das aus Microsoft 365
|
||||
übernommen. **Abmelden** beendet die CRM-Sitzung, nicht die bei Microsoft.
|
||||
Einzelheiten: [13 Anmeldung über Microsoft](13-anmeldung-sso.md).
|
||||
|
||||
Sagt die Meldung „Benutzername oder Passwort ist falsch", bleibt bewusst offen,
|
||||
welches von beidem nicht stimmte - so kann über die Anmeldemaske niemand
|
||||
Benutzernamen durchprobieren.
|
||||
|
||||
Es gibt zwei Rollen: **Administrator** sieht zusätzlich die Nutzerverwaltung und
|
||||
die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich.
|
||||
Es gibt zwei Rollen: **Administrator** sieht zusätzlich die Nutzerverwaltung,
|
||||
die Anmelde- und die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich.
|
||||
|
||||
## Sich zurechtfinden
|
||||
|
||||
|
||||
@@ -197,6 +197,7 @@ Diese Punkte haben real Zeit gekostet:
|
||||
| **`sequelize.sync({ alter: true })`** | Legt bei jedem Start Unique-Indizes neu an und sprengt irgendwann MySQLs Grenze von 64 Keys. Nur `sync()` benutzen. |
|
||||
| **Kaskaden beim Löschen** | Einen Suchagenten löschen löscht alle seine Treffer mit. Inaktiv setzen ist meist gemeint. |
|
||||
| **Doppelte Scanner-Läufe** | Scheduler und *Jetzt scannen* teilen eine Sperre. Ohne sie zahlt man jede KI-Bewertung zweimal. |
|
||||
| **Anmeldetoken in der Adresszeile** | Die Rückleitung von Microsoft ist ein `GET`. Stünde das CRM-Token darin, läge es in Browserverlauf und Apache-Log. Deshalb kommt ein Einmal-Ticket zurück, das der Browser per `POST` tauscht - siehe [13 Anmeldung über Microsoft](13-anmeldung-sso.md). |
|
||||
| **`allow-same-origin` bei `srcDoc`** | Ein so eingebettetes Dokument erbt die Origin der Anwendung und kann `localStorage` lesen - also das Anmeldetoken. Fremdes HTML gehört in `sandbox=""` ohne jedes Flag. Der Preis: die Inhaltshöhe ist nicht auslesbar, deshalb hat der Mailrahmen eine feste Höhe. |
|
||||
| **`/me` unter App-only** | Es gibt keinen Benutzer im Token, also auch kein `/me` - jeder Graph-Aufruf muss `/users/{postfach}/…` lauten. Genau daran ist das Altsystem gescheitert, still, weil die Antwort nicht geprüft wurde. |
|
||||
| **UTC neben Wandzeit** | `crm_mail_messages.received_at` steht in UTC (so liefert Graph es), `crm_events.starts_at` in lokaler Wandzeit. Beim Lesen im Frontend `parseMailDate()` benutzen, sonst ist die Anzeige um Stunden verschoben. |
|
||||
|
||||
+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 |
|
||||
|
||||
@@ -51,6 +51,11 @@ Genau **eine** Berechtigung, **ein** Zustimmungsklick, **eine**
|
||||
Zugriffsrichtlinie. Kein Schreibrecht, kein Redirect-URI, kein interaktiver
|
||||
Login.
|
||||
|
||||
> Dieselbe App-Registrierung trägt seit dem 21.09.2026 zusätzlich die
|
||||
> [Anmeldung über Microsoft](13-anmeldung-sso.md). Die braucht einen
|
||||
> Redirect-URI und delegierte Berechtigungen - beides betrifft nur den
|
||||
> Anmeldeweg, die hier beschriebene App-only-Anbindung bleibt unberührt.
|
||||
|
||||
1. **App-Registrierung** in Entra (Name z. B. `perinco CRM`), Kontotyp „nur
|
||||
dieses Verzeichnis", Redirect-URI leer lassen. Anwendungs-ID und
|
||||
Verzeichnis-ID notieren.
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# 13 Anmeldung über Microsoft (SSO)
|
||||
|
||||
Wer ein Microsoft-365-Konto von perinco hat, meldet sich im CRM mit einem Klick
|
||||
an - ohne zweites Passwort. Die Anmeldung mit Benutzername und Passwort bleibt
|
||||
daneben bestehen.
|
||||
|
||||
## Auf einen Blick
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Verfahren | OpenID Connect, Authorization Code Flow mit PKCE |
|
||||
| Wo läuft es | vollständig im Backend; der Browser sieht keine Tenant- oder Client-ID |
|
||||
| App-Registrierung | dieselbe wie die [Postfachanbindung](11-postfach.md) - kein zweites Secret |
|
||||
| Neue Konten | **werden nicht angelegt**; es braucht ein bestehendes Konto in der Nutzerverwaltung |
|
||||
| Passwortanmeldung | bleibt für alle bestehen (Notzugang, falls Entra ausfällt) |
|
||||
| Sitzung | wie bisher ein CRM-JWT aus `signToken()`, Standard 12 h |
|
||||
|
||||
## Ablauf
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as Browser
|
||||
participant API as Backend
|
||||
participant E as Entra
|
||||
participant G as Microsoft Graph
|
||||
participant DB as MariaDB
|
||||
|
||||
B->>API: GET /api/auth/sso/start
|
||||
Note right of API: state, nonce,<br/>PKCE-Verifier merken
|
||||
API-->>B: 302 zu login.microsoftonline.com
|
||||
B->>E: Anmeldung, Kontoauswahl
|
||||
E-->>B: 302 /api/auth/sso/callback?code&state
|
||||
B->>API: GET /callback
|
||||
API->>API: state verbrauchen (gilt einmal)
|
||||
API->>E: code + Verifier + Client-Secret
|
||||
E-->>API: id_token, access_token
|
||||
API->>API: aud, tid, nonce, exp prüfen
|
||||
API->>G: GET /me (delegiert)
|
||||
G-->>API: Objekt-ID, Name, Adresse
|
||||
API->>DB: Konto über entra_oid, sonst über E-Mail
|
||||
DB-->>API: Benutzer oder nichts
|
||||
API->>G: GET /me/photos/96x96/$value
|
||||
API-->>B: 302 /?sso_ticket=...
|
||||
B->>API: POST /api/auth/sso/ticket
|
||||
API-->>B: CRM-Token + Benutzer
|
||||
```
|
||||
|
||||
### Warum ein Ticket und kein Token in der Adresszeile
|
||||
|
||||
Die Rückleitung von Microsoft ist ein `GET`. Stünde das CRM-Token darin, läge es
|
||||
im Browserverlauf, im Apache-Log und in jedem weitergegebenen Link. Stattdessen
|
||||
kommt ein Einmal-Ticket zurück, das der Browser sofort per `POST` gegen das
|
||||
Token tauscht. Das Ticket gilt 60 Sekunden und genau einmal; `auth.tsx` räumt die
|
||||
Adresszeile anschliessend mit `history.replaceState()` auf.
|
||||
|
||||
### Warum zusätzlich `/me` bei Graph
|
||||
|
||||
Das ID-Token kommt über TLS direkt vom Token-Endpunkt und ist damit
|
||||
vertrauenswürdig - OIDC Core 3.1.3.7 erlaubt in diesem Fall den Verzicht auf die
|
||||
Signaturprüfung, deshalb braucht das Backend keine JWKS-Bibliothek. Geprüft
|
||||
werden trotzdem `aud`, `tid`, `nonce` und `exp`. Der Graph-Aufruf kommt dazu,
|
||||
weil er zweierlei belegt: dass das Zugriffstoken wirklich gilt, und welche
|
||||
Objekt-ID zum Konto gehört.
|
||||
|
||||
## Zuordnung zum CRM-Konto
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Profil von Graph:<br/>Objekt-ID, E-Mail"] --> B{"crm_users.entra_oid<br/>= Objekt-ID?"}
|
||||
B -->|ja| Z["anmelden"]
|
||||
B -->|nein| C{"crm_users.email = Adresse<br/>und entra_oid leer?"}
|
||||
C -->|ja| D["entra_oid festschreiben"] --> Z
|
||||
C -->|nein| E["abweisen:<br/>kein CRM-Konto"]
|
||||
Z --> F{"is_active?"}
|
||||
F -->|nein| G["abweisen:<br/>Konto deaktiviert"]
|
||||
```
|
||||
|
||||
Die Spalte `crm_users.entra_oid` (Unique) ist die dauerhafte Verknüpfung: die
|
||||
E-Mail-Adresse eines Menschen kann sich ändern, die Objekt-ID des
|
||||
Microsoft-Kontos nicht. Sie wird bei der ersten Anmeldung gesetzt und danach
|
||||
nicht mehr über die Adresse gesucht.
|
||||
|
||||
Ein Konto, das bereits an einer **anderen** Objekt-ID hängt, wird nicht
|
||||
übernommen - die Bedingung `entra_oid IS NULL` schliesst das aus.
|
||||
|
||||
### Profilfoto
|
||||
|
||||
Hat der Benutzer im CRM noch kein Foto, holt die erste Anmeldung
|
||||
`/me/photos/96x96/$value` und legt es als Data-URL in `crm_users.avatar` ab -
|
||||
dieselbe Spalte, die auch der Bild-Upload und
|
||||
[der LinkedIn-Weg](09-quellcode.md) benutzen. Ein selbst gesetztes Foto bleibt
|
||||
unangetastet, und wer kein Foto in M365 hat, meldet sich trotzdem an (Graph
|
||||
antwortet dann mit 404, das ist eingeplant).
|
||||
|
||||
## Endpunkte
|
||||
|
||||
| Methode | Pfad | Zugriff | Zweck |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/auth/sso/status` | offen | `{ enabled }` - zeigt die Anmeldemaske den Knopf? |
|
||||
| `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 |
|
||||
|
||||
Scheitert etwas, landet der Browser auf `/?sso_error=<Meldung>`; die
|
||||
Anmeldemaske zeigt den Text im gewohnten roten Kasten.
|
||||
|
||||
## Einrichtung in Entra
|
||||
|
||||
Die App-Registrierung **perinco crm** existiert bereits für die
|
||||
Postfachanbindung. Für die Anmeldung kommen zwei Dinge dazu.
|
||||
|
||||
**1. Umleitungsadresse.** *App-Registrierungen → perinco crm → Authentifizierung
|
||||
→ Plattform hinzufügen → Web*:
|
||||
|
||||
```
|
||||
https://crm.perinco.de/api/auth/sso/callback
|
||||
```
|
||||
|
||||
Zeichengenau, sonst antwortet Entra mit `AADSTS50011`. Front-Channel-Abmelde-URL
|
||||
bleibt leer, die Haken bei *Implizite Genehmigung* bleiben **aus** - der Code
|
||||
Flow braucht sie nicht. Für die lokale Entwicklung zusätzlich
|
||||
`http://localhost:3100/api/auth/sso/callback`; `http` erlaubt Entra nur für
|
||||
localhost.
|
||||
|
||||
**2. Delegierte Berechtigungen** mit Administratorzustimmung:
|
||||
|
||||
| Berechtigung | Wofür |
|
||||
| --- | --- |
|
||||
| `openid`, `profile` | Anmeldung, Grunddaten |
|
||||
| `User.Read` | `/me` - Objekt-ID, Name, Adresse |
|
||||
| `ProfilePhoto.Read.All` | Profilfoto beim ersten Anmelden |
|
||||
|
||||
Die Anwendungsberechtigung `Mail.Read` gehört zur Postfachanbindung und hat mit
|
||||
der Anmeldung nichts zu tun.
|
||||
|
||||
Weiter empfohlen: *Unterstützte Kontotypen* auf den eigenen Mandanten begrenzen
|
||||
und *Öffentliche Clientflows zulassen* auf **Nein** - das Backend ist ein
|
||||
vertraulicher Client mit Secret.
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
`server/.env`, Vorlage in `server/.env.example`.
|
||||
|
||||
| Variable | Pflicht | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `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 |
|
||||
| `MS_TENANT_ID`, `MS_CLIENT_ID`, `MS_CLIENT_SECRET` | ja | dieselben Werte wie beim Postfach |
|
||||
|
||||
> Die Werte werden **beim Start** gelesen. Nach einer Änderung an der `.env`
|
||||
> muss `perinco-crm-backend` neu starten. Fehlt etwas, bleibt die Anmeldung aus
|
||||
> und der Einstellungsreiter *Anmeldung* nennt die fehlende Variable beim Namen -
|
||||
> genau wie beim Postfach.
|
||||
|
||||
## Was das Backend nicht tut
|
||||
|
||||
* **Keine Konten anlegen.** Wer im CRM kein Konto hat, wird abgewiesen - mit dem
|
||||
Hinweis, sich an einen Administrator zu wenden. Zugriff vergibt weiterhin die
|
||||
Nutzerverwaltung.
|
||||
* **Keine Rollen aus Entra übernehmen.** `admin` bleibt eine Entscheidung im CRM.
|
||||
* **Keine Cookies.** Die Sitzung ist dasselbe JWT wie bei der Passwortanmeldung,
|
||||
gespeichert im `localStorage`.
|
||||
* **Kein Refresh-Token verwenden.** Entra stellt zwar eines aus, das CRM legt es
|
||||
nicht ab; die Sitzungsdauer regelt allein `JWT_EXPIRES_IN`.
|
||||
* **Keine Abmeldung bei Microsoft.** *Abmelden* beendet die CRM-Sitzung; das
|
||||
Microsoft-Konto bleibt im Browser angemeldet.
|
||||
|
||||
## Störungssuche
|
||||
|
||||
| Symptom | Ursache |
|
||||
| --- | --- |
|
||||
| Kein Microsoft-Knopf auf der Anmeldemaske | `MS_SSO_ENABLED` ist nicht `true` oder eine Variable fehlt - Reiter *Einstellungen → Anmeldung* zeigt welche |
|
||||
| `AADSTS50011: redirect_uri mismatch` | `MS_SSO_REDIRECT_URI` und der Eintrag in Entra weichen ab (Schrägstrich, `http`/`https`, Gross-/Kleinschreibung) |
|
||||
| `AADSTS7000215: Invalid client secret` | `MS_CLIENT_SECRET` abgelaufen oder falsch - betrifft dann auch das Postfach |
|
||||
| „Für … gibt es kein CRM-Konto" | kein Benutzer mit dieser E-Mail-Adresse; in der Nutzerverwaltung anlegen |
|
||||
| „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 |
|
||||
|
||||
## Quellen im Code
|
||||
|
||||
| Datei | Inhalt |
|
||||
| --- | --- |
|
||||
| `server/src/sso/config.js` | Variablen, `SSO_CONFIGURED`, Domainprüfung |
|
||||
| `server/src/sso/flow.js` | PKCE, Authorize-URL, Token-Tausch, `/me`, Foto, Einmal-Tickets |
|
||||
| `server/src/routes/auth.js` | die fünf Endpunkte und die Zuordnung `findSsoUser()` |
|
||||
| `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 |
|
||||
| `server/test/sso.test.mjs` | Prüfungen ohne Netz und Datenbank |
|
||||
@@ -22,6 +22,7 @@ keine Vorkenntnisse voraus.
|
||||
| [10 Glossar](10-glossar.md) | Agent, Match, Lead, Opportunity, Status- und Portalwerte |
|
||||
| [11 Postfachanbindung](11-postfach.md) | crm@perinco.de über Microsoft Graph, Kontakte aus Mails |
|
||||
| [12 Globale Suche](12-suche.md) | Volltext und erweiterte Suche im Kopf der Anwendung |
|
||||
| [13 Anmeldung über Microsoft](13-anmeldung-sso.md) | SSO mit Entra, Zuordnung der Konten, Einrichtung |
|
||||
|
||||
Die Designentscheidungen und die Messwerte zur Spracherkennung stehen im
|
||||
[README des Projekts](../README.md).
|
||||
|
||||
Reference in New Issue
Block a user