Files
alpenwerk-hr/docs/entra-sso.md
Maximilian Stubhan 730521ee79 Keep the environment's identifiers out of the repository
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>
2026-07-30 18:39:20 +02:00

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:

  1. Zertifikate & Geheimnisse → neues Client-Geheimnis. Der Wert wird gebraucht, nicht die Geheimnis-ID, und er ist nur einmal sichtbar.
  2. API-Berechtigungenopenid, profile, email (Microsoft Graph, delegiert), Administratorzustimmung erteilen.
  3. 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/callback und https://<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 beschreibbarsupabase.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 <e-mail> 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.