Supabase Auth – Tiefenkonfiguration: OAuth, SSO und Berechtigungssteuerung

An einem Nachmittag kam Sales zu mir: „Der Kunde will Login über Okta – SSO muss in zwei Wochen live sein.“ Ich war kurz sprachlos – meine App kannte nur E-Mail-Registrierung und Google OAuth. SAML? Unbekanntes Terrain.
In B2B-SaaS ist das Alltag. Enterprise-Kunden akzeptieren keine separaten Konten für jeden Mitarbeiter. Sie haben zentrale IdPs (Okta, Azure AD, Google Workspace): ein Klick zum Login, und beim Austritt wird der Zugriff überall entzogen.
Dieser Artikel ist für genau dieses Szenario. Wir starten mit OAuth-Social-Login, gehen zu SAML-SSO über und schließen mit Row Level Security (RLS) für Mandantenisolation ab – ein durchgängiges Auth- und Autorisierungskonzept von Consumer bis Enterprise.
1. OAuth – mehrere Provider in der Praxis
OAuth-Social-Login ist der Einstieg für die meisten Apps. Nutzer wollen keine Passwörter merken, Sie wollen keine Passwortspeicherung – Google oder GitHub übernehmen das. Beide Seiten profitieren.
Supabase unterstützt viele OAuth-Provider: Google, GitHub, Apple, Facebook, Discord, Twitter … In Produktion dominieren Google und GitHub; für iOS-Apps ist Apple Pflicht (App-Store-Richtlinien). Wir beginnen mit Google.
Google OAuth konfigurieren
Öffnen Sie die Google Cloud Console – die Menüs wirken unübersichtlich. Suchen Sie direkt nach „OAuth Client ID“.
Wählen Sie Web application und tragen Sie die Authorized redirect URI ein:
https://<ihre-projekt-ref>.supabase.co/auth/v1/callback
Diese URL empfängt den OAuth-Callback von Supabase Auth. Die Projekt-Ref steht oben links im Supabase Dashboard, z. B. abcdefghijklmnop.
Mit Client ID und Client Secret gehen Sie ins Supabase Dashboard unter Authentication > Providers, aktivieren Google und tragen die Werte ein.
Der Code-Aufruf ist schlank:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://abcdefghijklmnop.supabase.co',
'your-anon-key'
)
// OAuth-Login starten
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://your-app.com/auth/callback',
scopes: 'email profile'
}
})
if (error) {
console.error('Anmeldung fehlgeschlagen:', error.message)
return
}
// data.url ist die Weiterleitungs-URL – window.location.href = data.url
redirectTo ist die URL Ihrer App für das Login-Ergebnis; Supabase übergibt code und state. Auf dieser Seite rufen Sie supabase.auth.exchangeCodeForSession() auf:
// Auf der Seite /auth/callback
const { error } = await supabase.auth.exchangeCodeForSession()
if (!error) {
// Erfolg – Weiterleitung zur Startseite
window.location.href = '/'
}
GitHub OAuth konfigurieren
Ähnlicher Ablauf unter Settings > Developer settings > OAuth Apps. Authorization callback URL wieder https://<projekt-ref>.supabase.co/auth/v1/callback.
Unterschied: GitHub hat keine Scopes-Oberfläche – Scopes setzen Sie im Code:
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'https://your-app.com/auth/callback',
scopes: 'repo user' // repo für Zugriff auf private Repos
}
})
Für reines Login reichen die Standard-Scopes. Für GitHub-Daten (z. B. Repo-Liste) brauchen Sie repo.
Apple OAuth konfigurieren
Apple ist am aufwendigsten: Services ID im Apple Developer Portal, privater Schlüssel (.p8, nur einmal downloadbar – Verlust bedeutet Neuerzeugung).
Wichtige Parameter:
- Services ID: entspricht der Client ID
- Team ID: unter Membership
- Key ID: ID des Schlüssels
- Private Key: Inhalt der .p8-Datei
Im Supabase Dashboard den Private Key vollständig einfügen (inkl. BEGIN/END-Zeilen).
Aufruf wie bei Google/GitHub:
await supabase.auth.signInWithOAuth({
provider: 'apple',
options: {
redirectTo: 'https://your-app.com/auth/callback'
}
})
Für iOS gibt es eine Alternative: natives Sign in with Apple, dann Token an Supabase:
// identity_token von Apple im Frontend
const { data, error } = await supabase.auth.signInWithIdTokenCredentials({
provider: 'apple',
token: identityToken
})
Praktisch, wenn die native Apple-Anmeldung bereits integriert ist.
2. SAML-SSO für Unternehmen
Zurück zum Anfang: Der Kunde will Okta. Dafür reicht OAuth nicht – Sie brauchen SAML 2.0 SSO.
SAML funktioniert anders als OAuth: Nicht der Nutzer autorisiert Ihre App, sondern der Identity Provider (IdP) sendet Identitätsdaten an Ihre Anwendung (Service Provider, SP). Für Unternehmen ist das kontrollierbarer – Konto im IdP deaktiviert, Zugriff in allen angebundenen Apps weg.
Vorbereitung
Besorgen Sie die IdP-Metadata vom Kunden – Zertifikat, Endpunkte usw. Okta, Azure AD und Google Workspace exportieren sie.
Wichtige Supabase-URLs:
EntityID (SP-Kennung):
https://<projekt-ref>.supabase.co/auth/v1/sso/saml/metadata
ACS URL (empfängt SAML Response):
https://<projekt-ref>.supabase.co/auth/v1/sso/saml/acs
Metadata URL (Download):
https://<projekt-ref>.supabase.co/auth/v1/sso/saml/metadata?download=true
Diese URLs geben Sie dem IT-Admin für die SAML-App in Okta/Azure AD.
SSO per Supabase CLI
Das Dashboard unterstützt SAML inzwischen auch – für wiederholtes Feintuning bevorzuge ich die CLI.
# SAML-Verbindung hinzufügen
supabase sso add --type saml --project-ref <projekt-ref> \
--metadata-url 'https://company.okta.com/app/exk123/saml/samlmetadata' \
--domains company.com
--domains ist entscheidend: Nutzer mit @company.com nutzen diese SAML-Verbindung.
Alternativ --metadata-file statt URL:
supabase sso add --type saml --project-ref <projekt-ref> \
--metadata-file ./okta-metadata.xml \
--domains company.com
Die Ausgabe enthält eine sso_provider_id (z. B. abc123def456) – wichtig für RLS-Isolation.
Attribute Mapping
Die SAML Response liefert E-Mail, Name, Abteilung – Feldnamen variieren. Mapping definieren Sie in JSON:
{
"keys": {
"email": {
"name": "email",
"names": ["EmailAddress", "email", "mail"],
"required": true
},
"first_name": {
"name": "first_name",
"names": ["FirstName", "givenName", "first_name"]
},
"last_name": {
"name": "last_name",
"names": ["LastName", "surname", "last_name"]
}
}
}
Anwenden mit supabase sso update:
supabase sso update <sso_provider_id> \
--project-ref <projekt-ref> \
--attribute-mapping-file ./mapping.json
Multi-Tenant-SSO
Mehrere SAML-Verbindungen pro Projekt sind möglich – z. B. Acme Corp (Okta) und Globex Inc (Azure AD):
# SSO für Acme Corp
supabase sso add --type saml --project-ref <projekt-ref> \
--metadata-url 'https://acme.okta.com/.../metadata' \
--domains acme.com
# Rückgabe sso_provider_id: provider_abc
# SSO für Globex Inc
supabase sso add --type saml --project-ref <projekt-ref> \
--metadata-url 'https://globex.azure.com/.../metadata' \
--domains globex.com
# Rückgabe sso_provider_id: provider_def
Acme-Nutzer laufen über provider_abc, Globex über provider_def – getrennte Nutzerdaten pro Verbindung.
Login-Erlebnis
Nach der Konfiguration:
- Nutzer gibt E-Mail ein (z. B.
[email protected]) - Supabase erkennt SSO für
acme.com - Weiterleitung zur Okta-Anmeldung
- Anmeldung bei Okta (oft schon eingeloggt)
- SAML Response an Supabase
- Session wird erstellt, Rücksprung in Ihre App
Code zum Starten:
// Nach E-Mail-Eingabe
const { data, error } = await supabase.auth.signInWithSSO({
domain: 'acme.com'
})
if (data?.url) {
window.location.href = data.url
}
Oder direkt mit sso_provider_id:
const { data, error } = await supabase.auth.signInWithSSO({
providerId: 'provider_abc'
})
Besonderheiten von SSO-Nutzern
SSO-Nutzer unterscheiden sich von „normalen“ Konten:
- E-Mail vom IdP: nicht in der App änderbar
- Kein Passwort in Supabase: Verifikation liegt beim IdP
- E-Mail als verifiziert: IdP hat bereits geprüft
E-Mail-Änderungen laufen über den IT-Admin in Okta/Azure AD.
3. Row Level Security – fortgeschritten
Authentifizierung klärt „wer ist der Nutzer?“, Autorisierung „was darf er?“.
Klassisch prüft jede API die Berechtigung – repetitiv, fehleranfällig, teuer in der Datenbank.
PostgreSQL Row Level Security (RLS) verlagert die Prüfung in die Datenbank. Jede Abfrage wird automatisch gefiltert.
Standard nach Aktivierung
Häufige Falle: Nach ENABLE ROW LEVEL SECURITY ist alles gesperrt.
-- RLS aktivieren
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Jede Abfrage liefert nichts (auch für Admins)
SELECT * FROM posts; -- 0 Zeilen
Erst Policies erlauben Zugriff.
USING und WITH CHECK
USING: Darf der Nutzer die Zeile „sehen“ oder ansprechen? Für SELECT, UPDATE, DELETE.
WITH CHECK: Darf er so schreiben? Für INSERT, UPDATE – der neue Zustand muss die Bedingung erfüllen.
-- Nutzer verwalten nur eigene Posts
CREATE POLICY "Users manage own posts" ON posts
FOR ALL TO authenticated
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- Nur Lesen
CREATE POLICY "Users view own posts" ON posts
FOR SELECT TO authenticated
USING (auth.uid() = author_id);
-- Nur Einfügen
CREATE POLICY "Users insert posts" ON posts
FOR INSERT TO authenticated
WITH CHECK (auth.uid() = author_id);
auth.uid() ist die UUID des eingeloggten Nutzers; TO authenticated schränkt auf angemeldete Nutzer ein.
RESTRICTIVE vs PERMISSIVE
Mehrere Policies pro Tabelle sind möglich. Standard: PERMISSIVE – eine erfüllte Policy reicht.
-- Policy 1: eigene Daten
CREATE POLICY "Own data" ON posts
FOR SELECT USING (auth.uid() = author_id);
-- Policy 2: öffentliche Posts
CREATE POLICY "Public posts" ON posts
FOR SELECT USING (is_public = true);
-- Zwei PERMISSIVE Policies: eine reicht
RESTRICTIVE Policies müssen zusätzlich erfüllt sein – oft als globaler Filter:
-- Globaler Mandantenfilter
CREATE POLICY "Tenant isolation" ON posts
AS RESTRICTIVE TO authenticated
USING (tenant_id = (
SELECT tenant_id FROM users WHERE id = auth.uid()
));
-- PERMISSIVE für konkrete Aktionen
CREATE POLICY "Authors edit own posts" ON posts
FOR UPDATE USING (auth.uid() = author_id);
Ergebnis: richtiger Mandant (RESTRICTIVE) und Autor beim Bearbeiten (PERMISSIVE).
Vollständiges Multi-Tenant-Muster
SaaS mit einem Mandanten pro Kunde – tenant_id in Nutzer- und Geschäftstabellen:
-- Nutzertabelle
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
email TEXT,
sso_provider_id TEXT -- nur bei SSO
);
-- Geschäftstabelle
CREATE TABLE projects (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
name TEXT,
created_by UUID REFERENCES users(id)
);
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Globaler Mandantenfilter (RESTRICTIVE)
CREATE POLICY "Tenant isolation" ON projects
AS RESTRICTIVE TO authenticated
USING (tenant_id = (
SELECT tenant_id FROM users WHERE id = auth.uid()
));
-- Operationen (PERMISSIVE)
CREATE POLICY "Tenant users can view" ON projects
FOR SELECT TO authenticated
USING (true);
CREATE POLICY "Project creators can edit" ON projects
FOR UPDATE TO authenticated
USING (created_by = auth.uid());
Selbst SELECT * FROM projects liefert nur Daten des aktuellen Mandanten.
SSO und Mandantenisolation
Für SSO-Nutzer hilft sso_provider_id. Im JWT steht die Login-Methode:
-- RLS für SSO-Nutzer
CREATE POLICY "SSO tenant isolation" ON organization_settings
AS RESTRICTIVE TO authenticated
USING (sso_provider_id = (
SELECT auth.jwt()#>>'{amr,0,provider}'
));
auth.jwt()#>>'{amr,0,provider}' liefert bei SSO die sso_provider_id.
Performance
RLS wirkt wie implizites WHERE – oft mit Subqueries. Optimierung:
1. Indizes auf Policy-Feldern
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_projects_tenant ON projects(tenant_id);
2. Subqueries in Policies vermeiden
Pro Zeile teuer. Besser tenant_id im JWT:
-- Ungeeignet: Subquery
USING (tenant_id = (SELECT tenant_id FROM users WHERE id = auth.uid()))
-- Empfohlen: JWT
USING (tenant_id = (auth.jwt()->>'tenant_id')::uuid)
Dafür der Custom Access Token Hook (nächstes Kapitel).
3. SECURITY DEFINER-Funktionen
CREATE FUNCTION current_tenant_id() RETURNS UUID
LANGUAGE SQL STABLE SECURITY DEFINER AS $$
SELECT tenant_id FROM users WHERE id = auth.uid();
$$;
CREATE POLICY "Tenant isolation" ON projects
AS RESTRICTIVE USING (tenant_id = current_tenant_id());
STABLE erlaubt PostgreSQL, die Funktion pro Transaktion einmal auszuführen.
4. Custom Claims und RBAC
Standard-JWT enthält Nutzer-ID, E-Mail, Rolle (authenticated/anon). Oft brauchen Sie mehr: Rolle (admin/moderator), Mandanten-ID, Berechtigungsliste.
Der Custom Access Token Hook passt das JWT vor der Ausstellung an.
Typische Einsatzfälle
- RBAC: Rolle im JWT, Policy prüft darüber
- Multi-Tenant:
tenant_idim JWT statt Subquery - Kleineres JWT: unnötige Felder entfernen – relevant bei SSR und großen Cookies
Supabase-JWTs sind standardmäßig umfangreich (session_id, aal, amr …). Bei vielen SSR-Seiten summiert sich das im Cookie.
Hook implementieren
PL/pgSQL-Funktion vor JWT-Erzeugung:
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
claims jsonb;
user_role text;
tenant_id uuid;
BEGIN
claims := event->'claims';
SELECT role INTO user_role
FROM public.user_roles
WHERE user_id = (event->>'user_id')::uuid;
IF user_role IS NOT NULL THEN
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
END IF;
SELECT tenant_id INTO tenant_id
FROM public.users
WHERE id = (event->>'user_id')::uuid;
IF tenant_id IS NOT NULL THEN
claims := jsonb_set(claims, '{tenant_id}', to_jsonb(tenant_id));
END IF;
RETURN jsonb_build_object('claims', claims);
END;
$$;
GRANT EXECUTE ON FUNCTION public.custom_access_token_hook(jsonb)
TO supabase_auth_admin;
-- Dashboard: Authentication > Hooks > Custom Access Token
event enthält u. a. user_id, claims, authentication_method. Der zurückgegebene claims-Block landet im JWT.
RBAC-Schema
CREATE TYPE app_role AS ENUM ('admin', 'moderator', 'user');
CREATE TYPE app_permission AS ENUM (
'posts.delete',
'posts.pin',
'users.manage',
'settings.edit'
);
CREATE TABLE public.user_roles (
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
role app_role NOT NULL,
PRIMARY KEY (user_id, role)
);
CREATE TABLE public.role_permissions (
role app_role NOT NULL,
permission app_permission NOT NULL,
PRIMARY KEY (role, permission)
);
INSERT INTO role_permissions (role, permission) VALUES
('admin', 'posts.delete'),
('admin', 'posts.pin'),
('admin', 'users.manage'),
('admin', 'settings.edit'),
('moderator', 'posts.delete'),
('moderator', 'posts.pin');
authorize()-Funktion
CREATE OR REPLACE FUNCTION public.authorize(
requested_permission app_permission
)
RETURNS boolean
LANGUAGE plpgsql
STABLE SECURITY DEFINER
AS $$
DECLARE
user_role app_role;
BEGIN
SELECT (auth.jwt()->>'user_role')::app_role
INTO user_role;
IF user_role IS NULL THEN
RETURN false;
END IF;
RETURN EXISTS (
SELECT 1 FROM public.role_permissions
WHERE role = user_role
AND permission = requested_permission
);
END;
$$;
GRANT EXECUTE ON FUNCTION public.authorize(app_permission)
TO authenticated;
authorize() in Policies
CREATE POLICY "Role-based delete" ON posts
FOR DELETE TO authenticated
USING (
authorize('posts.delete') OR auth.uid() = author_id
);
CREATE POLICY "Admin pin posts" ON posts
FOR UPDATE TO authenticated
USING (
NOT is_pinned OR authorize('posts.pin')
);
Erste Policy: Löschen mit posts.delete oder als Autor. Zweite: is_pinned = true nur mit posts.pin.
Custom Claims im Frontend
import { jwtDecode } from 'jwt-decode'
const { data: { session } } = await supabase.auth.getSession()
if (session) {
const decoded = jwtDecode(session.access_token)
console.log('Rolle:', decoded.user_role)
console.log('Mandant:', decoded.tenant_id)
}
jwtDecode dekodiert nur – die Signaturprüfung erfolgt serverseitig bei Supabase.
5. Enterprise-SaaS – Gesamtszenario
OAuth, SSO, RLS und Custom Claims zusammen: Zwei Nutzergruppen:
- Enterprise: SSO (Okta/Azure AD), Daten einem Mandanten zugeordnet
- Privat: Google/GitHub OAuth, kein Mandant, nur öffentliche Ressourcen
Enterprise-Daten bleiben für OAuth-Nutzer unsichtbar.
Nutzertabelle
CREATE TABLE public.users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
auth_type TEXT NOT NULL DEFAULT 'oauth', -- 'oauth' oder 'sso'
sso_provider_id TEXT,
tenant_id UUID,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE public.tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
sso_provider_id TEXT NOT NULL UNIQUE,
plan_type TEXT NOT NULL DEFAULT 'team',
created_at TIMESTAMPTZ DEFAULT now()
);
Einheitlicher Custom Access Token Hook
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
claims jsonb;
user_record RECORD;
BEGIN
claims := event->'claims';
SELECT auth_type, sso_provider_id, tenant_id, role
INTO user_record
FROM public.users
WHERE id = (event->>'user_id')::uuid;
IF user_record IS NULL THEN
RETURN jsonb_build_object('claims', claims);
END IF;
claims := jsonb_set(claims, '{auth_type}', to_jsonb(user_record.auth_type));
IF user_record.auth_type = 'sso' THEN
IF user_record.tenant_id IS NOT NULL THEN
claims := jsonb_set(claims, '{tenant_id}', to_jsonb(user_record.tenant_id));
END IF;
IF user_record.sso_provider_id IS NOT NULL THEN
claims := jsonb_set(claims, '{sso_provider_id}', to_jsonb(user_record.sso_provider_id));
END IF;
END IF;
IF user_record.role IS NOT NULL THEN
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_record.role));
END IF;
RETURN jsonb_build_object('claims', claims);
END;
$$;
Nutzeranlage nach Login
OAuth und SSO legen auth.users an – danach synchronisieren Sie public.users:
const { data: { user } } = await supabase.auth.getUser()
if (user) {
const { data: existingUser } = await supabase
.from('users')
.select('id')
.eq('id', user.id)
.single()
if (!existingUser) {
const authType = user.app_metadata?.provider || 'oauth'
await supabase.from('users').insert({
id: user.id,
email: user.email,
auth_type: authType.startsWith('sso') ? 'sso' : 'oauth',
sso_provider_id: user.app_metadata?.sso_provider_id,
tenant_id: null,
full_name: user.user_metadata?.full_name,
avatar_url: user.user_metadata?.avatar_url
})
}
}
Einheitliche RLS für projects
CREATE TABLE public.projects (
id UUID PRIMARY KEY,
tenant_id UUID REFERENCES tenants(id),
name TEXT NOT NULL,
is_public BOOLEAN DEFAULT false,
created_by UUID REFERENCES users(id)
);
ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Tenant or public access" ON public.projects
AS RESTRICTIVE TO authenticated
USING (
(auth.jwt()->>'auth_type' = 'sso'
AND tenant_id = (auth.jwt()->>'tenant_id')::uuid)
OR
(auth.jwt()->>'auth_type' = 'oauth' AND is_public = true)
);
CREATE POLICY "Users can view" ON public.projects
FOR SELECT TO authenticated
USING (true);
CREATE POLICY "Tenant users can create" ON public.projects
FOR INSERT TO authenticated
WITH CHECK (
auth.jwt()->>'auth_type' = 'sso'
AND tenant_id = (auth.jwt()->>'tenant_id')::uuid
);
CREATE POLICY "Creators or admins can edit" ON public.projects
FOR UPDATE TO authenticated
USING (
created_by = auth.uid()
OR authorize('projects.edit')
);
Logik: SSO sieht nur den eigenen Mandanten; OAuth nur is_public; Erstellen nur SSO mit Mandant; Bearbeiten als Ersteller oder mit projects.edit.
Mandantenzuweisung durch Admin
Nach erstem SSO-Login ist tenant_id oft null:
UPDATE public.users
SET tenant_id = '<mandanten-uuid>'
WHERE id = '<nutzer-uuid>';
INSERT INTO public.user_roles (user_id, role)
VALUES ('<nutzer-uuid>', 'admin');
Per Admin-UI oder Auth Hook automatisch aus sso_provider_id ableiten.
Ablaufübersicht
Nutzer meldet sich an
│
├─ OAuth
│ │
│ ├─ Google/GitHub Callback
│ ├─ Supabase legt auth.users an
│ ├─ App legt public.users an (auth_type='oauth')
│ └─ JWT: { auth_type: 'oauth' }
│ │
│ └─ RLS: nur is_public=true
│
└─ SSO
│
├─ Okta/Azure AD SAML Callback
├─ auth.users mit sso_provider_id
├─ public.users (auth_type='sso')
├─ Admin setzt tenant_id
├─ Hook schreibt tenant_id ins JWT
└─ JWT: { auth_type: 'sso', tenant_id: '...' }
│
└─ RLS: nur passender tenant_id
Von OAuth über SAML-SSO bis RLS und Custom Claims – ein durchgängiges Modell für Consumer- bis Enterprise-Szenarien.
Entscheidungshilfen:
Überwiegend Privatnutzer: Google/GitHub-OAuth reicht – einfach, vertraut, günstig in der Wartung. RLS auf „eigene Daten“ genügt oft.
B2B-SaaS: SSO ist Pflicht. Okta, Azure AD und Google Workspace einplanen, Multi-Tenant von Anfang an mitdenken.
Komplexe Enterprise-Apps: RBAC plus RLS – Rollen-/Rechtetabellen, Custom Claims, authorize() – für feine Regeln wie „löschen ja, anpinnen nein“ oder „Mandant darf bearbeiten, nicht löschen“.
Pragmatischer Weg: Mit OAuth starten, Flow stabilisieren. Bei Enterprise-Nachfrage SSO und RBAC schichtweise ergänzen – Supabase erlaubt schrittweises Ausbauen ohne Big-Bang am ersten Tag.
Supabase Auth – Enterprise-Konfiguration von OAuth bis RLS
Schritt für Schritt von OAuth-Social-Login über SAML-SSO bis zur RLS-Mandantenisolation
⏱️ Estimated time: 2 hr
- 1
Step 1: OAuth-Provider konfigurieren (Google/GitHub/Apple)
1. OAuth-App in der Provider-Konsole anlegen (Google Cloud Console / GitHub Settings)
2. Callback-URL: https://<projekt-ref>.supabase.co/auth/v1/callback
3. Provider im Supabase Dashboard aktivieren, Client ID und Secret eintragen
4. signInWithOAuth() mit scopes und redirectTo aufrufen - 2
Step 2: SAML-SSO für Unternehmen einrichten
1. Metadata-Datei oder -URL vom IdP (Okta/Azure AD) beschaffen
2. SSO-Verbindung per CLI: supabase sso add --type saml --domains company.com
3. Attribute Mapping für E-Mail, Name usw. konfigurieren
4. Login testen: E-Mail eingeben, automatische Weiterleitung zum IdP - 3
Step 3: RLS-Mandantenisolation umsetzen
1. tenant_id in Geschäftstabellen ergänzen
2. RLS aktivieren: ALTER TABLE projects ENABLE ROW LEVEL SECURITY
3. RESTRICTIVE Policy als globalen Mandantenfilter anlegen
4. PERMISSIVE Policies für SELECT/INSERT/UPDATE
5. Index auf tenant_id für Performance - 4
Step 4: Custom Access Token Hook einrichten
1. Funktion public.custom_access_token_hook() anlegen
2. tenant_id, user_role usw. als Custom Claims setzen
3. supabase_auth_admin zur Ausführung berechtigen
4. Hook im Dashboard aktivieren (Authentication > Hooks)
5. Frontend liest Claims per jwtDecode() - 5
Step 5: RBAC-Berechtigungssteuerung implementieren
1. Tabellen user_roles und role_permissions anlegen
2. ENUM-Typen app_role und app_permission definieren
3. authorize()-Funktion für Berechtigungsprüfung
4. In RLS Policies authorize('permission.name') nutzen
5. UI anhand von user_role im JWT ein-/ausblenden
FAQ
Was ist der Unterschied zwischen OAuth und SAML SSO – was soll ich wählen?
Für B2B-SaaS ist SSO Pflicht; für reine Privatprodukte reicht OAuth.
Nach Aktivierung von RLS liefert die Abfrage keine Zeilen – was tun?
RLS-Policies sind langsam – wie optimieren?
• Index auf Policy-Feldern: CREATE INDEX idx_tenant ON projects(tenant_id);
• Keine Subqueries in Policies: tenant_id per Custom Access Token Hook ins JWT, Auslesen mit auth.jwt()->>'tenant_id'
• Komplexe Logik in SECURITY DEFINER-Funktion auslagern – PostgreSQL führt sie einmal aus
Wie speichere ich Custom Fields wie tenant_id oder user_role im JWT?
1. PL/pgSQL-Funktion custom_access_token_hook(event jsonb)
2. Claims mit jsonb_set() erweitern
3. Hook im Dashboard aktivieren (Authentication > Hooks > Custom Access Token)
4. Frontend liest Custom Claims mit jwtDecode()
Wie trenne ich Enterprise- und Privatnutzer in einem Multi-Tenant-SaaS?
• SSO-Nutzer: nur Daten des passenden tenant_id
• OAuth-Nutzer: nur Daten mit is_public=true
RESTRICTIVE Policy als globaler Filter, PERMISSIVE Policies für konkrete Operationen.
Unterstützt Supabase Apple Sign In – ist die Einrichtung aufwendig?
1. Services ID im Apple Developer Portal
2. Privaten Schlüssel erzeugen (.p8, nur einmal downloadbar)
3. Team ID, Key ID, Services ID und Schlüssel im Supabase Dashboard eintragen
iOS-Apps können die native Sign in with Apple API nutzen und identity_token an signInWithIdTokenCredentials() übergeben.
Der Kunde verlangt SSO, aber der IdP-Typ ist unbekannt – was tun?
13 Min. Lesezeit · Veröffentlicht am: 21. Apr. 2026 · Aktualisiert am: 14. Juli 2026
Supabase in der Praxis
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Supabase Edge Functions in der Praxis: Deno-Runtime und TypeScript-Entwicklungsleitfaden
Supabase Edge Functions vertieft: Deno-Runtime-Architektur und V8-Isolate-Prinzip, CLI-Workflow, RESTful API mit Hono – von lokalem Debugging bis Produktions-Deployment
Teil 8 von 10
Nächster
Supabase Edge Functions in der Praxis: Deno-Runtime und globale Edge-Bereitstellung
Supabase Edge Functions laufen auf der Deno-Runtime an globalen Edge-Knoten mit Cold Start in 120 ms. ESZip-Architektur, Deno-Vorteile, Praxisbeispiele und Vergleich mit Cloudflare Workers.
Teil 10 von 10
Ähnliche Beiträge
Supabase Einstieg: PostgreSQL + Auth + Storage als All-in-One-Backend

Supabase Einstieg: PostgreSQL + Auth + Storage als All-in-One-Backend
Supabase-Datenbankdesign: Tabellenstruktur, Beziehungen und Row Level Security – vollständiger Leitfaden


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen