Projekt-Ref, Entra-Client- und Tenant-ID standen im Klartext in der SSO-Anleitung. Geheimnisse sind das nicht — ohne Schlüssel gibt eine Projekt-URL nichts her, und RLS greift unabhängig davon. Sie zeigen aber auf die laufende Umgebung, und dieses Repository wandert weiter als sie: es geht gleich auf einen eigenen Git-Server und später an den Kunden. Jetzt Platzhalter; die Werte gehören in die Übergabedokumentation. In der Historie stehen sie weiterhin — das sauber zu entfernen hiesse, die Historie neu zu schreiben, und das passiert nicht nebenbei. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.0 KiB
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://<projekt-ref>.supabase.co/auth/v1/callback |
| Anwendungs-ID (Client) | <client-id> |
| Verzeichnis-ID (Mandant) | <tenant-id> |
Die konkreten Werte stehen bewusst nicht hier, sondern in der Übergabedokumentation. Sie sind zwar keine Geheimnisse — ohne Schlüssel gibt eine Projekt-URL nichts her, und RLS greift ohnehin —, aber sie zeigen auf die echte Umgebung, und dieses Repository wandert weiter als sie.
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:
- Zertifikate & Geheimnisse → neues Client-Geheimnis. Der Wert wird gebraucht, nicht die Geheimnis-ID, und er ist nur einmal sichtbar.
- API-Berechtigungen →
openid,profile,email(Microsoft Graph, delegiert), Administratorzustimmung erteilen. - Tokenkonfiguration → Gruppenanspruch, siehe unten.
Einrichtung in Supabase
Authentication → Providers → Azure:
| Feld | Wert |
|---|---|
| Application (Client) ID | <client-id> |
| Secret Value | der Wert aus „Zertifikate & Geheimnisse" |
| Azure Tenant URL | https://login.microsoftonline.com/<tenant-id> |
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/callbackundhttps://<produktion>/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:
- SSO in Betrieb nehmen, einmal anmelden.
node --env-file=.env.local supabase/entra-claims.ts <e-mail>zeigt, was inidentity_datagelandet ist.- 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.