# Anmeldung über Entra ID Die Anwendung meldet ausschliesslich über Microsoft Entra ID an. Die Sitzungsverwaltung macht **Auth.js** (`auth.ts`, `lib/auth/config.ts`) — es gibt keinen Anmeldedienst eines Anbieters mehr dazwischen. **Warum das trotzdem eine kleine Änderung ist:** die Anmeldung liefert nach wie vor nur eine UUID. `profiles.id` trägt weiterhin `role` und `is_active`, und damit bleiben `is_hr_user()` und alle 58 RLS-Policies unverändert gültig. Die Sicherheitsgrenze wandert nicht in den Anwendungscode. ## Einrichtung im Entra-Mandanten App-Registrierung, einmalig — angelegt im Mandanten *loudspring management GmbH*: | | | |---|---| | Name | Alpenwerk HR | | Kontotypen | Nur ein Mandant | | Umleitungs-URI (Web) | `https:///api/auth/callback/microsoft-entra-id` | | Anwendungs-ID (Client) | `` | | Verzeichnis-ID (Mandant) | `` | Die konkreten Werte stehen bewusst nicht hier, sondern in der Übergabedokumentation. Sie sind zwar keine Geheimnisse — ohne Client-Geheimnis gibt eine ID nichts her, und RLS greift ohnehin —, aber sie zeigen auf die echte Umgebung, und dieses Repository wandert weiter als sie. Die Umleitungs-URI zeigt jetzt auf die **Anwendung selbst**. Für die lokale Entwicklung kommt `http://localhost:3000/api/auth/callback/microsoft-entra-id` als zweite URI dazu; Entra erlaubt `http` nur für `localhost`. Danach: 1. **Zertifikate & Geheimnisse** → neues Client-Geheimnis. Der *Wert* wird gebraucht, nicht die Geheimnis-ID, und er ist nur einmal sichtbar. 2. **API-Berechtigungen** → `openid`, `profile`, `email` (Microsoft Graph, delegiert), Administratorzustimmung erteilen. `User.Read` wird **nicht** gebraucht: der eingebaute Anbieter von Auth.js fordert es an, um das Profilbild aus dem Graph zu holen — `lib/auth/config.ts` schaltet beides ab. 3. **Tokenkonfiguration** → Gruppenanspruch, siehe unten. ## Konfiguration der Anwendung Vier Werte, alle server-seitig — nichts davon landet im Browser-Bundle: | Variable | Wert | |---|---| | `AUTH_SECRET` | `npx auth secret` oder `openssl rand -base64 32` | | `AUTH_MICROSOFT_ENTRA_ID_ID` | Anwendungs-ID (Client) | | `AUTH_MICROSOFT_ENTRA_ID_SECRET` | der Wert aus „Zertifikate & Geheimnisse" | | `AUTH_MICROSOFT_ENTRA_ID_ISSUER` | `https://login.microsoftonline.com//v2.0` | Der Aussteller ist bei „Nur ein Mandant" **nicht optional**. Bleibt er leer, benutzt Auth.js `common` — dann dürfte sich jedes Microsoft-Konto anmelden, auch ein privates Outlook-Konto. Die Freischaltung über `profiles` fängt das zwar ab, aber die Eingangstür soll erst gar nicht so weit offenstehen. `AUTH_SECRET` verschlüsselt das Sitzungscookie. Ein Wechsel meldet alle ab — im Ernstfall genau das gewünschte Mittel. ## Was bei der ersten Anmeldung passiert 1. Auth.js prüft das Token von Entra (`state`, `nonce`, Signatur, Aussteller). 2. Der Rückruf in `auth.ts` nimmt daraus die **`oid`** — nicht `sub`, nicht die E-Mail. Die `oid` identifiziert dieselbe Person über Anwendungen hinweg und überlebt Namens- und Adressänderungen. 3. `app_upsert_user(oid, email, name)` legt die `app_users`-Zeile an und liefert die Kennung, die von da an in jeder Transaktion als `app.user_id` steht. 4. **Gibt es zu der Adresse bereits ein `profiles`-Eintrag, übernimmt die Funktion dessen Kennung** statt eine neue zu vergeben. Das ist der Grund, warum bestehende Zugänge nach der Umstellung weiterlaufen: Notizen, Entwürfe und Protokolleinträge hängen an dieser ID. Der Abgleich über die Adresse ist genau hier vertretbar und sonst nirgends: sie kommt aus einem von Entra ausgestellten Token, nicht aus einem Formular. Wer sie behauptet, hat sie bereits bewiesen. ## Freischaltung über die Entra-Gruppe Wer sich anmeldet, hat damit **noch keinen Zugriff**. Zugriff hat, wer eine `profiles`-Zeile mit `role = 'hr'` und `is_active = true` besitzt. ### Woher der Gruppen-Anspruch kommt Entra schickt Gruppen nur mit, wenn es in der Tokenkonfiguration eingestellt ist. Zwei Varianten: | Variante | Lizenz | Haken | |---|---|---| | Sicherheitsgruppen | frei | Schickt *alle* Sicherheitsgruppen mit. Ab etwa 200 Gruppen liefert Entra statt der Liste einen Verweis, und die Auswertung greift ins Leere. | | Der Anwendung zugewiesene Gruppen | Entra ID P1 | Nur die zugewiesene Gruppe steht im Token. | ### Wo die Auswertung hingehört In den `jwt`-Rückruf in `auth.ts`, neben `app_upsert_user()` — dort liegt `profile.groups` aus dem ID-Token vor. Das ist belastbar, und der Grund ist wichtig: das ID-Token ist von Entra signiert und wurde von Auth.js gegen den Aussteller geprüft. Die angemeldete Person kann seinen Inhalt nicht beeinflussen. (Unter GoTrue war dieselbe Stelle eine Falle: `auth.users.raw_user_meta_data` war von der Person selbst beschreibbar, und eine Freischaltung, die von dort gelesen hätte, wäre selbstbedienbar gewesen.) ### Reihenfolge Gebaut wird das erst, wenn feststeht, wie der Anspruch tatsächlich ankommt — das hängt an der gewählten Variante und an der Konfiguration des Mandanten. Ablauf: 1. SSO in Betrieb nehmen, einmal anmelden. 2. Im `jwt`-Rückruf einmalig `console.log(profile)` — das zeigt die Ansprüche so, wie der Mandant sie tatsächlich schickt. 3. Erst dann die Auswertung mit der konkreten Gruppen-ID schreiben. Ohne Schritt 2 wäre sie geraten. Bis dahin wird `profiles` von Hand gepflegt. ### Was die Gruppe nicht kann Die Mitgliedschaft steht im Token. Wer aus der Gruppe entfernt wird, verliert den Zugriff deshalb **bei der nächsten Anmeldung**, nicht sofort. Für den sofortigen Entzug bleibt `profiles.is_active = false` das Mittel — das wirkt beim nächsten Datenbankzugriff, weil `is_hr_user()` die Spalte je Abfrage liest. ## Wer prüft was | Stelle | Prüft | Wann | |---|---|---| | `proxy.ts` | Gibt es überhaupt eine Sitzung? | jede Anfrage | | `app/(app)/layout.tsx` | `profiles.role` / `is_active` | jeder Seitenaufbau | | `lib/auth/require-hr.ts` | dasselbe, für `/api/export/*` | jeder Aufruf | | RLS-Policies | `is_hr_user()` | jede einzelne Abfrage | Der Proxy prüft die HR-Rechte **nicht** — er hat keine Datenbankverbindung. Sie in das Sitzungstoken zu schreiben wäre schneller gewesen und hätte eine Behauptung eingefroren: eine entzogene Freischaltung wirkte dann erst mit dem nächsten Token. Bei einer Personalanwendung ist das die falsche Richtung.