Changer le thème

Supabase Auth : OAuth, SSO et contrôle des accès en profondeur

Easton editorial illustration: solo-founder business system console

Ce jour-là, un collègue commercial est passé me voir : « Le client veut se connecter avec Okta, il faut livrer le SSO sous deux semaines. » J’ai figé un instant — mon appli ne gérait que l’inscription par e-mail et Google OAuth. SAML ? Jamais touché.

En B2B SaaS, c’est le quotidien. Les clients entreprise n’acceptent pas que chaque employé crée un compte chez vous : ils ont Okta, Azure AD ou Google Workspace, veulent une redirection en un clic, et que le compte soit coupé dès le départ.

Cet article est fait pour ce cas. On part de la connexion sociale OAuth, on enchaîne sur l’intégration SAML SSO, puis on verrouille l’isolation multi-tenant avec Row Level Security (RLS) — un schéma complet d’authentification et d’autorisation, du grand public à l’entreprise.

1. OAuth : configuration multi-fournisseurs

La connexion sociale OAuth est le point de départ de la plupart des apps. L’utilisateur ne veut pas mémoriser un mot de passe ; vous ne voulez pas gérer le stockage — Google ou GitHub s’en charge, tout le monde y gagne.

Supabase supporte de nombreux fournisseurs : Google, GitHub, Apple, Facebook, Discord, Twitter… En production, Google et GitHub dominent ; Apple est quasi obligatoire sur iOS (exigence App Store). Commençons par Google.

Configuration Google OAuth

Ouvrez Google Cloud Console — les menus peuvent impressionner. Cherchez directement « OAuth Client ID ».

Choisissez Web application et renseignez l’Authorized redirect URI :

https://<ref-de-votre-projet>.supabase.co/auth/v1/callback

C’est l’endpoint où Supabase Auth reçoit le callback OAuth. Le project ref est en haut à gauche du Dashboard, par ex. abcdefghijklmnop.

Avec Client ID et Client Secret, activez Google dans Authentication > Providers et collez les valeurs.

Côté code :

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  'https://abcdefghijklmnop.supabase.co',
  'your-anon-key'
)

// Lancer la connexion OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://your-app.com/auth/callback',
    scopes: 'email profile'
  }
})

if (error) {
  console.error('Échec de connexion :', error.message)
  return
}

// data.url : redirection — window.location.href = data.url

redirectTo est l’URL de votre app qui reçoit code et state. Sur cette page, appelez exchangeCodeForSession() :

// Page /auth/callback
const { error } = await supabase.auth.exchangeCodeForSession()
if (!error) {
  window.location.href = '/'
}

Configuration GitHub OAuth

Même logique : Settings > Developer settings > OAuth Apps. Authorization callback URL identique : https://<ref-projet>.supabase.co/auth/v1/callback.

GitHub n’a pas d’écran scopes dans l’app — on les passe dans le code :

await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: 'https://your-app.com/auth/callback',
    scopes: 'repo user'  // repo pour accéder aux dépôts privés
  }
})

Pour une simple connexion utilisateur, les scopes par défaut suffisent. Pour synchroniser des dépôts GitHub, ajoutez repo.

Configuration Apple OAuth

Apple est la plus pénible : Services ID sur Apple Developer Portal, clé privée .p8 (téléchargement unique — perdue, il faut régénérer).

Paramètres clés :

  • Services ID : équivalent Client ID
  • Team ID : page Membership
  • Key ID : identifiant de la clé
  • Private Key : contenu complet du .p8 (lignes BEGIN/END incluses)

Dans le Dashboard Supabase, collez tout le fichier pour Private Key.

await supabase.auth.signInWithOAuth({
  provider: 'apple',
  options: {
    redirectTo: 'https://your-app.com/auth/callback'
  }
})

Sur iOS natif, récupérez l’identity token et passez-le à Supabase :

const { data, error } = await supabase.auth.signInWithIdTokenCredentials({
  provider: 'apple',
  token: identityToken
})

Utile si Sign in with Apple est déjà intégré nativement.

2. Intégration SAML SSO entreprise

Retour au scénario d’ouverture : le client veut Okta. Là, OAuth ne suffit plus — il faut SAML 2.0 SSO.

Le modèle diffère : OAuth autorise une app tierce à accéder aux données de l’utilisateur ; SAML envoie l’identité de l’IdP entreprise vers votre application (Service Provider, SP). Pour l’entreprise, SAML est plus maîtrisable — compte désactivé dans l’IdP, toutes les apps coupent l’accès.

Avant de configurer

Il vous faut les Metadata de l’IdP (certificats, endpoints). Okta, Azure AD et Google Workspace les exportent tous.

URLs Supabase essentielles :

EntityID (identifiant SP) :
https://<ref-projet>.supabase.co/auth/v1/sso/saml/metadata

ACS URL (réception SAML Response) :
https://<ref-projet>.supabase.co/auth/v1/sso/saml/acs

Metadata (téléchargement) :
https://<ref-projet>.supabase.co/auth/v1/sso/saml/metadata?download=true

Transmettez-les à l’IT du client pour créer l’app SAML dans Okta ou Azure AD.

Configuration SSO avec la CLI Supabase

Le Dashboard gère aussi le SAML ; je préfère la CLI pour itérer avec les clients entreprise.

# Ajouter une connexion SAML
supabase sso add --type saml --project-ref <ref-projet> \
  --metadata-url 'https://company.okta.com/app/exk123/saml/samlmetadata' \
  --domains company.com

--domains indique à Supabase : les e-mails @company.com passent par cette connexion SAML.

Alternative avec fichier :

supabase sso add --type saml --project-ref <ref-projet> \
  --metadata-file ./okta-metadata.xml \
  --domains company.com

La commande renvoie un sso_provider_id (ex. abc123def456) — clé pour l’isolation RLS.

Attribute Mapping

Le SAML Response contient e-mail, nom, département, etc., avec des noms de champs variables. Définissez le mapping :

{
  "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"]
    }
  }
}

Puis :

supabase sso update <sso_provider_id> \
  --project-ref <ref-projet> \
  --attribute-mapping-file ./mapping.json

SSO multi-tenant

Plusieurs connexions SAML par projet — Acme Corp (Okta) et Globex Inc (Azure AD) :

supabase sso add --type saml --project-ref <ref-projet> \
  --metadata-url 'https://acme.okta.com/.../metadata' \
  --domains acme.com
# sso_provider_id: provider_abc

supabase sso add --type saml --project-ref <ref-projet> \
  --metadata-url 'https://globex.azure.com/.../metadata' \
  --domains globex.com
# sso_provider_id: provider_def

Les employés @acme.com utilisent provider_abc, ceux de @globex.com provider_def. Données isolées par connexion.

Parcours utilisateur

  1. Saisie de l’e-mail ([email protected])
  2. Supabase détecte le SSO pour acme.com
  3. Redirection vers Okta Acme
  4. Authentification (souvent déjà connecté)
  5. SAML Response vers Supabase, session créée, retour app
const { data, error } = await supabase.auth.signInWithSSO({
  domain: 'acme.com'
})

if (data?.url) {
  window.location.href = data.url
}

Ou directement :

const { data, error } = await supabase.auth.signInWithSSO({
  providerId: 'provider_abc'
})

Spécificités des utilisateurs SSO

  • E-mail géré par l’IdP : pas de changement dans votre app
  • Pas de mot de passe : validation côté IdP uniquement
  • E-mail considéré comme vérifié : déjà validé par l’IdP

Changement d’e-mail = action IT dans Okta ou Azure AD.

3. Row Level Security : usages avancés

L’authentification répond à « qui est l’utilisateur » ; l’autorisation à « que peut-il faire ».

Classiquement, chaque API vérifie les droits — code dupliqué, oublis, requêtes supplémentaires à chaque appel.

Avec Row Level Security (RLS) PostgreSQL, le filtre descend en base : chaque requête est restreinte sans logique métier répétée.

Comportement par défaut après activation

Piège fréquent : tout est refusé par défaut.

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

SELECT * FROM posts;  -- 0 ligne, même pour un admin

Il faut créer des Policy pour autoriser l’accès.

USING et WITH CHECK

USING : l’utilisateur peut-il « voir » ou cibler la ligne ? (SELECT, UPDATE, DELETE)

WITH CHECK : peut-il « écrire » une ligne qui respecte la condition ? (INSERT, UPDATE)

CREATE POLICY "Users manage own posts" ON posts
FOR ALL TO authenticated
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);

CREATE POLICY "Users view own posts" ON posts
FOR SELECT TO authenticated
USING (auth.uid() = author_id);

CREATE POLICY "Users insert posts" ON posts
FOR INSERT TO authenticated
WITH CHECK (auth.uid() = author_id);

auth.uid() renvoie l’UUID de l’utilisateur connecté ; TO authenticated limite aux sessions authentifiées.

RESTRICTIVE vs PERMISSIVE

Par défaut, les Policy sont PERMISSIVE : une seule suffit.

CREATE POLICY "Own data" ON posts
FOR SELECT USING (auth.uid() = author_id);

CREATE POLICY "Public posts" ON posts
FOR SELECT USING (is_public = true);
-- PERMISSIVE : l'une OU l'autre

RESTRICTIVE : condition obligatoire en plus des PERMISSIVE.

CREATE POLICY "Tenant isolation" ON posts
AS RESTRICTIVE TO authenticated
USING (tenant_id = (
  SELECT tenant_id FROM users WHERE id = auth.uid()
));

CREATE POLICY "Authors edit own posts" ON posts
FOR UPDATE USING (auth.uid() = author_id);

Effet : bon tenant (RESTRICTIVE) et auteur pour modifier (PERMISSIVE).

Modèle d’isolation multi-tenant

Chaque client entreprise = un tenant ; tenant_id sur users et tables métier.

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL,
  email TEXT,
  sso_provider_id TEXT
);

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;

CREATE POLICY "Tenant isolation" ON projects
AS RESTRICTIVE TO authenticated
USING (tenant_id = (
  SELECT tenant_id FROM users WHERE id = auth.uid()
));

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());

Même un SELECT * FROM projects ne renvoie que les lignes du tenant courant.

Isolation des utilisateurs SSO

Le JWT expose le mode de connexion :

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}' extrait l’identifiant du fournisseur SSO.

Optimisation des performances

Les Policy ajoutent un WHERE implicite, parfois avec sous-requêtes.

1. Indexer les colonnes des Policy

CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_projects_tenant ON projects(tenant_id);

2. Éviter les sous-requêtes par ligne

-- Déconseillé
USING (tenant_id = (SELECT tenant_id FROM users WHERE id = auth.uid()))

-- Recommandé : tenant_id dans le JWT
USING (tenant_id = (auth.jwt()->>'tenant_id')::uuid)

Voir la section Custom Access Token Hook ci-dessous.

3. Fonctions SECURITY DEFINER

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 permet à PostgreSQL d’exécuter la fonction une fois par transaction.

4. Custom Claims et RBAC

Le JWT Supabase contient par défaut id, e-mail, rôle (authenticated/anon). Souvent il faut aussi rôle métier (admin/moderator), tenant_id, liste de permissions.

Le Custom Access Token Hook modifie le JWT avant émission.

Pourquoi des Custom Claims

  1. RBAC : rôle dans le JWT, Policy basées sur authorize()
  2. Multi-tenant : tenant_id sans sous-requête dans chaque Policy
  3. Taille JWT : retirer des champs inutiles en SSR (session_id, aal, amr…)

Implémentation du Hook

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

L’event contient user_id, claims, authentication_method (password, oauth, sso/saml…).

Schéma RBAC

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');

Fonction authorize()

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;

Policy avec authorize()

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')
);

Suppression : permission posts.delete ou auteur du post. Épinglage : posts.pin requis pour passer is_pinned à true.

Lecture côté front

import { jwtDecode } from 'jwt-decode'

const { data: { session } } = await supabase.auth.getSession()

if (session) {
  const decoded = jwtDecode(session.access_token)
  console.log('Rôle :', decoded.user_role)
  console.log('Tenant :', decoded.tenant_id)
}

jwtDecode ne vérifie pas la signature — c’est le rôle du serveur Supabase.

5. Scénario SaaS entreprise complet

Combinons tout pour un produit avec deux profils :

  1. Utilisateurs entreprise : SSO client (Okta/Azure AD), rattachés à un tenant
  2. Utilisateurs individuels : OAuth Google/GitHub, sans tenant, ressources publiques uniquement

Les données entreprise restent invisibles pour les comptes OAuth.

Schéma users

CREATE TABLE public.users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL,
  auth_type TEXT NOT NULL DEFAULT 'oauth',
  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()
);

Hook unifié

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;
$$;

Synchronisation après connexion

OAuth et SSO créent une ligne dans auth.users. Synchronisez vers 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
    })
  }
}

Policy unifiées sur 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')
);
  • SSO : projets du tenant uniquement
  • OAuth : projets is_public = true
  • Création réservée au SSO
  • Édition : créateur ou projects.edit

Attribution du tenant par l’admin

À la première connexion SSO, tenant_id peut être null :

UPDATE public.users
SET tenant_id = '<UUID-du-tenant>'
WHERE id = '<UUID-utilisateur>';

INSERT INTO public.user_roles (user_id, role)
VALUES ('<UUID-utilisateur>', 'admin');

Interface admin ou Auth Hook pour mapper automatiquement provider → tenant.

Schéma du flux

Connexion

   ├─ OAuth
   │    ├─ Callback Google/GitHub
   │    ├─ auth.users créé
   │    ├─ public.users (auth_type='oauth')
   │    └─ JWT { auth_type: 'oauth' } → RLS : is_public=true

   └─ SSO
        ├─ Callback SAML Okta/Azure AD
        ├─ auth.users (+ sso_provider_id)
        ├─ public.users (auth_type='sso')
        ├─ Admin assigne tenant_id
        ├─ Hook ajoute tenant_id au JWT
        └─ JWT { auth_type: 'sso', tenant_id } → RLS : filtre tenant

Synthèse

De OAuth à SAML SSO, puis RLS et Custom Claims : un schéma qui couvre le produit individuel comme le SaaS B2B.

Grand public : Google/GitHub OAuth, configuration légère, RLS « chacun ses données ».

B2B SaaS : SSO quasi obligatoire — sans lui, beaucoup de RFP s’arrêtent là. Prévoyez Okta, Azure AD, Google Workspace et l’architecture multi-tenant.

Applications entreprise complexes : RBAC + RLS — tables rôles/permissions, Custom Claims, authorize() pour des règles fines (« supprimer oui, épingler non »).

Conseil pratique : démarrez avec OAuth et un RLS simple. Ajoutez SSO et RBAC quand les clients entreprise arrivent. L’architecture Supabase se prête à cette montée en charge progressive.

Configuration Supabase Auth de niveau entreprise

De la connexion sociale OAuth à l'intégration SAML SSO, jusqu'à l'isolation multi-tenant avec RLS

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: Configurer les OAuth Provider (Google, GitHub, Apple)

    1. Créer une OAuth App dans la console du fournisseur (Google Cloud Console / GitHub Settings)
    2. Définir l'URL de callback : https://<ref-projet>.supabase.co/auth/v1/callback
    3. Activer le Provider dans le Dashboard Supabase, renseigner Client ID et Secret
    4. Appeler signInWithOAuth() en précisant scopes et redirectTo
  2. 2

    Step 2: Configurer l'intégration SAML SSO entreprise

    1. Récupérer le fichier ou l'URL Metadata auprès de l'IdP (Okta / Azure AD)
    2. Ajouter la connexion SSO : supabase sso add --type saml --domains company.com
    3. Configurer l'Attribute Mapping (email, name, etc.)
    4. Tester : après saisie de l'e-mail, redirection automatique vers l'IdP
  3. 3

    Step 3: Mettre en place l'isolation multi-tenant avec RLS

    1. Ajouter tenant_id sur les tables métier
    2. Activer RLS : ALTER TABLE projects ENABLE ROW LEVEL SECURITY
    3. Créer une Policy RESTRICTIVE pour le filtre locataire global
    4. Créer des Policy PERMISSIVE pour SELECT / INSERT / UPDATE
    5. Indexer tenant_id pour les performances
  4. 4

    Step 4: Configurer le Custom Access Token Hook

    1. Créer la fonction public.custom_access_token_hook()
    2. Y ajouter tenant_id, user_role et autres custom claims
    3. Accorder l'exécution à supabase_auth_admin
    4. Activer le Hook dans le Dashboard (Authentication > Hooks)
    5. Lire les custom claims côté front avec jwtDecode()
  5. 5

    Step 5: Implémenter le contrôle RBAC

    1. Créer les tables user_roles et role_permissions
    2. Définir les types enum app_role et app_permission
    3. Créer la fonction authorize() pour vérifier les droits
    4. Appeler authorize('permission.name') dans les Policy RLS
    5. Afficher ou masquer l'UI selon user_role dans le JWT

FAQ

Quelle différence entre OAuth et SAML SSO ? Lequel choisir ?
OAuth convient aux applications grand public : connexion Google/GitHub, configuration simple, faible coût de maintenance. SAML SSO convient au B2B : les employés se connectent avec le compte d'entreprise (Okta / Azure AD), géré par l'IT ; à la sortie, le compte est révoqué automatiquement.

Pour un SaaS B2B, le SSO est quasi indispensable ; pour un produit individuel, OAuth suffit.
Après activation du RLS, les requêtes renvoient vide — que faire ?
Comportement par défaut du RLS : tout est refusé tant qu'aucune Policy n'existe. Exemple : CREATE POLICY "Users view own posts" ON posts FOR SELECT TO authenticated USING (auth.uid() = author_id);
Les Policy RLS sont lentes — comment optimiser ?
Trois leviers :

• Indexer les champs utilisés dans les Policy : CREATE INDEX idx_tenant ON projects(tenant_id);
• Éviter les sous-requêtes : placer tenant_id dans le JWT via Custom Access Token Hook, puis auth.jwt()->>'tenant_id'
• Encapsuler la logique complexe dans une fonction SECURITY DEFINER (une seule exécution optimisée par PostgreSQL)
Comment stocker tenant_id ou user_role dans le JWT ?
Utiliser le Custom Access Token Hook Supabase :

1. Fonction PL/pgSQL custom_access_token_hook(event jsonb)
2. Ajouter les claims avec jsonb_set()
3. Activer le Hook (Authentication > Hooks > Custom Access Token)
4. Décoder côté front avec jwtDecode()
Comment isoler utilisateurs entreprise et particuliers dans un SaaS multi-tenant ?
Stocker auth_type ('oauth' ou 'sso') et tenant_id dans le JWT ; les Policy filtrent en conséquence :

• Utilisateurs SSO : données du tenant correspondant
• Utilisateurs OAuth : données avec is_public=true uniquement

RESTRICTIVE comme filtre global, PERMISSIVE pour les opérations concrètes.
Supabase prend-il en charge Apple Sign In ? Est-ce complexe ?
Oui, plus que Google/GitHub :

1. Créer un Services ID sur Apple Developer Portal
2. Générer une clé privée (.p8, téléchargement unique)
3. Renseigner Team ID, Key ID, Services ID et contenu de la clé dans le Dashboard

Sur iOS, l'API native Sign in with Apple puis signInWithIdTokenCredentials() avec identity_token.
Le client exige le SSO mais je ne connais pas son IdP — que faire ?
Demander à l'admin IT le fichier ou l'URL Metadata (Okta, Azure AD, Google Workspace exportent tous les deux). Puis supabase sso add --metadata-file : Supabase parse endpoints et certificats automatiquement.

13 min de lecture · Publié le: 21 avr. 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog