# Anmeldung über Entra ID Die Anwendung meldet ausschliesslich über Microsoft Entra ID an. Supabase Auth bleibt dabei die Sitzungsverwaltung — Entra ist der Anbieter, nicht der Ersatz. **Das ist der Grund, warum der Umstieg klein ist:** `auth.uid()` liefert weiterhin 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://wcqzamhttnzhionftyoe.supabase.co/auth/v1/callback` | | Anwendungs-ID (Client) | `88037b8a-54f6-488f-8fcd-67d72b311a88` | | Verzeichnis-ID (Mandant) | `b002d91e-4569-4911-8756-3dcdc47152bb` | Die beiden IDs sind Kennungen, keine Geheimnisse — sie stehen deshalb hier. Der *Wert* des Client-Geheimnisses gehört ausschliesslich ins Supabase-Feld „Secret Value" und in keine Datei im Projekt. Die Umleitungs-URI ist **Supabases** Callback, nicht der der Anwendung. Der eigene Callback (`/auth/callback`) steht nur in der Redirect-Allowlist des Supabase-Projekts. 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. 3. **Tokenkonfiguration** → Gruppenanspruch, siehe unten. ## Einrichtung in Supabase Authentication → Providers → Azure: | Feld | Wert | |---|---| | Application (Client) ID | `88037b8a-54f6-488f-8fcd-67d72b311a88` | | Secret Value | der Wert aus „Zertifikate & Geheimnisse" | | Azure Tenant URL | `https://login.microsoftonline.com/b002d91e-4569-4911-8756-3dcdc47152bb` | Die Tenant URL ist bei „Nur ein Mandant" nicht optional. Bleibt sie leer, benutzt Supabase `common`, und Entra weist die Anmeldung ab, weil die Registrierung nur den eigenen Mandanten akzeptiert. Authentication → URL Configuration: - Site URL: die Produktions-URL - Redirect URLs: `http://localhost:3000/auth/callback` und `https:///auth/callback` ## 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. Diese Zeile entsteht aus der Mitgliedschaft in einer Entra-Gruppe. ### 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. | ### Warum die Auswertung aus `auth.identities` liest, nicht aus `auth.users` Das ist kein Detail, sondern der Kern der Absicherung. `auth.users.raw_user_meta_data` ist **von der angemeldeten Person selbst beschreibbar** — `supabase.auth.updateUser({ data: … })` schreibt genau dorthin. Läse die Freischaltung von dort, könnte sich jede:r Angemeldete den HR-Anspruch selbst eintragen und hätte damit Zugriff auf sämtliche Personaldaten. `auth.identities.identity_data` schreibt ausschliesslich GoTrue aus der Antwort des Anbieters. Nur das ist eine belastbare Quelle. ### Reihenfolge Der Trigger wird erst gebaut, 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. `node --env-file=.env.local supabase/entra-claims.ts ` zeigt, was in `identity_data` gelandet ist. 3. Erst dann die Migration mit der konkreten Gruppen-ID schreiben. Ohne Schritt 2 wäre die Migration geraten. ### 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. ## Bestehende Zugänge Ein bestehendes Konto mit Passwort-Anmeldung und ein Entra-Konto derselben Person sind für Supabase **zwei verschiedene Benutzer** mit verschiedenen IDs. Die `profiles`-Zeile hängt an der alten ID; nach der ersten Entra-Anmeldung zeigt sie ins Leere und die Person ist ausgesperrt. `supabase/relink-profile.ts` hängt sie um. Es überträgt auch die Fremdschlüssel, die auf die alte Benutzer-ID zeigen (`audit_log.actor_user_id`, `employee_notes.author_user_id`, …), sonst stünde in der Historie eine Kennung, zu der es kein Konto mehr gibt.