# V6.0.0 — SSO / SAML : Enterprise Authentication > **Statut** : ✅ **Livré en v6.7.0** (2026-09-24) — conception historique ci-dessous > **Date** : 2026-09-15 > **Route** : `feat/v6-sso-saml` → `develop` → `main` > **Dépendances** : v4.0.0 Accounts & Integrations (OAuth2 Gitea/GitHub existant) --- ## 1. Vision & objectifs Ajouter le support **SSO/SAML 2.0** pour permettre aux entreprises d'intégrer FlowDeck dans leur infrastructure d'authentification existante. Les utilisateurs d'une organisation peuvent se connecter via leur fournisseur d'identité (IdP) d'entreprise sans avoir de compte local séparé. ### Objectifs | Critère | Cible | |---------|-------| | Protocoles | SAML 2.0 (principal), OIDC (complémentaire) | | Fournisseurs supportés | Azure AD, Okta, Google Workspace, OneLogin, Keycloak, Auth0 | | Temps de connexion SSO | < 3 s (redirect + callback) | | Gestion des utilisateurs | Auto-provisioning à la première connexion | | Déconnexion | SLO (Single Logout) supporté | | Fallback | Login local toujours disponible (admin activé/désactivable) | --- ## 2. Architecture SSO/SAML ### 2.1 Flux SAML 2.0 ``` ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Utilisateur │ │ FlowDeck │ │ IdP │ │ (navigateur) │ │ (SP) │ │ (Azure AD / │ │ │ │ │ │ Okta / etc.)│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ 1. Accède à FlowDeck │ │ │───────────────────────►│ │ │ │ │ │ 2. Redirige vers IdP │ │ │◄───────────────────────│ │ │ │ │ │ 3. SSO redirect │ │ │───────────────────────►│────────────────────────►│ │ │ │ │ 5. Callback avec │ │ │ SAMLResponse │◄────────────────────────│ │ │ │ │ 6. POST /auth/saml/callback │ │───────────────────────►│ │ │ │ │ │ 7. Validate SAML │ │ │ → Create/Update │ │ │ user → Session │ │ │◄───────────────────────│ │ │ │ │ │ 8. Authentifié │ │ │◄───────────────────────│ │ ``` ### 2.2 Composants côté serveur ``` ┌───────────────────────────────────────────────────────────┐ │ FASTAPI (Python) │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ auth/routers/sso.py │ │ │ │ ├─ GET /auth/saml/login — initie la requête │ │ │ │ ├─ POST /auth/saml/callback — traite la réponse │ │ │ │ ├─ GET /auth/saml/metadata — SP metadata endpoint │ │ │ │ ├─ GET /auth/oidc/login — flux OIDC │ │ │ │ ├─ POST /auth/oidc/callback │ │ │ │ └─ POST /auth/sso/logout — SLO │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ auth/providers/saml_provider.py │ │ │ │ ├─ SAMLProvider — wrapper python3-saml │ │ │ │ └─ validate_saml_response() │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ auth/providers/oidc_provider.py │ │ │ │ ├─ OIDCProvider — wrapper httpx + OIDC lib │ │ │ │ └─ validate_oidc_token() │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ auth/session.py — SessionManager étendu │ │ │ │ ├─ create_sso_session() │ │ │ │ ├─ handle_sso_user() │ │ │ │ └─ enforce_sso_restriction() │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ services/sso_provisioning.py │ │ │ │ ├─ auto_provision_user() │ │ │ │ ├─ sync_user_attributes() │ │ │ │ └─ map_sso_groups_to_workspaces() │ │ │ └─────────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────────┘ ``` --- ## 3. Configuration SAML ### 3.1 Nouveau table `sso_config` ```sql CREATE TABLE sso_config ( id INTEGER PRIMARY KEY AUTOINCREMENT, workspace_id INTEGER REFERENCES workspaces(id) ON DELETE CASCADE, -- NULL = workspace-level; specific workspace; global if NULL+global flag provider_type TEXT NOT NULL, -- 'saml', 'oidc' entity_id TEXT NOT NULL, -- SAML: Entity ID of the IdP (e.g., "http://www.microsoft.com/...") sso_url TEXT NOT NULL, -- SAML: IdP SSO URL (HTTP-POST binding) slo_url TEXT, -- SAML: IdP Single Logout URL x509_certificate TEXT NOT NULL, -- SAML: IdP signing certificate (PEM format) -- OIDC: issuer URL issuer_url TEXT, -- OIDC: OIDC issuer identifier client_id TEXT, -- OIDC: client_id client_secret TEXT, -- OIDC: client_secret (encrypted at rest) scope TEXT DEFAULT 'openid profile email', -- OIDC: scopes requested attribute_mapping TEXT NOT NULL DEFAULT '{}', -- JSON: {"email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", ...} -- Maps SAML/OIDC attributes to FlowDeck user fields auto_provision BOOLEAN NOT NULL DEFAULT 1, -- Auto-create user on first SSO login default_workspace_id INTEGER, -- Workspace to assign new SSO users groups_mapping TEXT DEFAULT '[]', -- JSON: [{"sso_group": "Admins", "workspace_role": "admin"}, ...] active BOOLEAN NOT NULL DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by INTEGER REFERENCES users(id) ); ``` ### 3.2 Mapping des attributs Par défaut pour SAML (mappings standard OASIS) : ```json { "login": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "email": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "full_name": "urn:oasis:names:tc:SAML:attribute:displayName", "avatar_url": "urn:oasis:names:tc:SAML:attribute:thumbnail" } ``` Par défaut pour OIDC : ```json { "login": "sub", "email": "email", "full_name": "name", "avatar_url": "picture" } ``` ### 3.3 Configuration `.env` (exemples) ```env # SAML — Azure AD SSO_PROVIDER=saml SSO_ENTITY_ID=https://sts.windows.net/{tenant-id}/ SSO_SSO_URL=https://login.microsoftonline.com/{tenant-id}/saml2 SSO_SLO_URL=https://login.microsoftonline.com/{tenant-id}/saml2/logout SSO_X509_CERTIFICATE="-----BEGIN CERTIFICATE-----\n..." SSO_ATTRIBUTE_MAPPING={"login":"nameid","email":"email","full_name":"name"} SSO_AUTO_PROVISION=true SSO_DEFAULT_WORKSPACE_ID=1 SSO_GROUPS_MAPPING=[{"sso_group":"FlowDeck Admins","workspace_role":"admin"}] # OIDC — Google Workspace SSO_PROVIDER=oidc SSO_ISSUER_URL=https://accounts.google.com SSO_CLIENT_ID=xxx.apps.googleusercontent.com SSO_CLIENT_SECRET=yyy SSO_SCOPE=openid profile email ``` --- ## 4. Endpoints API ### 4.1 Routeur `app/routers/sso.py` ``` GET /auth/saml/login — Redirige vers l'IdP SAML POST /auth/saml/callback — Traite le SAMLResponse, crée la session GET /auth/saml/metadata — Retourne le SP metadata XML (pour configurer l'IdP) POST /auth/saml/logout — Initie le SLO (Single Logout) GET /auth/oidc/login — Redirige vers l'IdP OIDC POST /auth/oidc/callback — Traite le token OIDC, crée la session POST /auth/oidc/logout — SLO OIDC POST /api/v2/sso/config — Créer/configurer SSO (admin, scope admin) GET /api/v2/sso/config — Lire la config SSO courante PUT /api/v2/sso/config — Mettre à jour la config DELETE /api/v2/sso/config — Supprimer la config SSO (désactiver SSO) GET /api/v2/sso/workspaces — Lister les workspaces avec SSO actif POST /api/v2/sso/sync — Forcer la synchro des groupes/attributs ``` ### 4.2 Service `app/services/sso_provisioning.py` ```python class SSOService: """Provisioning et gestion des utilisateurs SSO.""" def handle_sso_login(self, sso_data: dict, provider_type: str) -> dict: """Traite un login SSO : trouve/crée l'utilisateur, crée la session.""" # 1. Extraire les attributs selon le provider # 2. Chercher l'utilisateur par email/login SSO # 3. Si trouvé → mise à jour des attributs # 4. Si non trouvé et auto_provision → création # 5. Assigner le workspace par défaut ou le premier disponible # 6. Vérifier les groupes SSO → rôle workspace # 7. Créer la session def sync_sso_groups(self, user_id: int, sso_groups: list[str]) -> None: """Sync les groupes SSO vers les rôles workspace.""" # Comparer sso_groups avec groups_mapping # Mettre à jour workspace_members.role def enforce_sso_restriction(self, workspace_id: int) -> bool: """Vérifie si le workspace est en mode SSO-only.""" # Si config.sso_only = True → interdire le login local ``` --- ## 5. Sécurité ### 5.1 Validation des assertions SAML | Validation | Détail | |------------|--------| | Signature | Vérifier la signature avec le certificat IdP | | `NotBefore` / `NotOnOrAfter` | Rejeter les assertions expirées | | `Audience` | Vérifier que l'audience match l'entity_id du SP | | `Destination` | Vérifier que le destination match notre callback URL | | `Issuer` | Vérifier que l'émetteur est le IdP attendu | | `InResponseTo` | Prévenir le replay attack | ### 5.2 Protection supplémentaire - **CSRF sur le callback** : le `RelayState` contient un token CSRF validé - **Rate limiting** sur les endpoints SSO : max 5 tentatives/minute - **Logging audit** : chaque login SSO est enregistré (user, provider, IP, timestamp) - **Session fixation** : nouvelle session créée après chaque SSO login - **Encryption** : `client_secret` chiffré avec `app_secret_key` avant stockage ### 5.3 Table `sso_login_history` ```sql CREATE TABLE sso_login_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, provider_type TEXT NOT NULL, provider_name TEXT NOT NULL, sso_identifier TEXT, -- email ou subject du SSO ip_address TEXT, user_agent TEXT, success BOOLEAN NOT NULL, error_message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_sso_history_user ON sso_login_history(user_id, created_at); ``` --- ## 6. Interface utilisateur ### 6.1 Login page — ajout SSO buttons ```html
ou continuez avec
``` ### 6.2 Settings — Configuration SSO (admin) Nouvelle section dans `settings.html` → onglet **"SSO / Enterprise"** : ``` ┌──────────────────────────────────────────────────────┐ │ SSO / Enterprise Authentication │ │───────────────────────────────────────────────────────│ │ Provider: [▼ SAML / OIDC] │ │ │ │ SAML Configuration: │ │ ├── Entity ID: [___________________________] │ │ ├── SSO URL: [___________________________] │ │ ├── SLO URL: [___________________________] │ │ ├── Certificate: [text area - PEM] │ │ └── Attribute Mapping: [JSON editor] │ │ │ │ OIDC Configuration: │ │ ├── Issuer URL: [_________________________] │ │ ├── Client ID: [_________________________] │ │ ├── Client Secret: [_______________________] │ │ └── Scope: [openid profile email] │ │ │ │ User Provisioning: │ │ ├── Auto-provision new users [✓] │ │ ├── Default workspace: [dropdown] │ │ └── Group mapping: [table editor] │ │ │ │ ┌─ Group Mappings ───────────────────────────────────┐ │ │ │ SSO Group │ FlowDeck Role │ Workspace │ │ │ │ FlowDeck Admins │ admin │ Main │ │ │ │ FlowDeck Members │ editor │ Main │ │ │ └────────────────────┴────────────────┴──────────────┘ │ │ │ │ [Save SSO Configuration] [Disable SSO] │ │ Status: ✅ Active — 12 users provisioned via SSO │ └──────────────────────────────────────────────────────┘ ``` ### 6.3 Métadonnées SP pour l'IdP Endpoint `GET /auth/saml/metadata` retourne le XML SAML metadata : ```xml ... ``` --- ## 7. Intégration avec le système existant ### 7.1 Comportement selon le type de compte ``` SSO ACTIVÉ + local auth ENABLED: → Boutons SSO visibles sur la login page → Login local disponible (optionnel, admin-toggle) → Les utilisateurs SSO et locaux coexistent SSO ACTIVÉ + local auth DISABLED: → Seuls les boutons SSO sont affichés → Login local caché / 403 pour /auth/local-login → Admin du workspace peut garder le sien SSO ACTIVÉ + compte local existant: → Email match → fusion automatique des sessions → Email ne match pas → login local séparé (deux comptes) ``` ### 7.2 Interaction avec le PermissionManager Le `PermissionManager` existant (`app/services/permission_manager.py`) est étendu : ```python class PermissionManager: # Méthodes existantes conservées # + Nouvelles méthodes SSO : def is_sso_only_workspace(self, workspace_id: int) -> bool: """Vérifie si le workspace exige SSO.""" def get_sso_roles(self, user_id: int, workspace_id: int) -> list[str]: """Retourne les rôles issus des groupes SSO.""" def sync_sso_permissions(self, user_id: int, sso_groups: list[str], workspace_id: int): """Met à jour les rôles basés sur les groupes SSO.""" ``` ### 7.3 Cookie de session SSO Le cookie `flowdeck_session` existant est utilisé tel quel. Une colonne supplémentaire dans la table `users` distingue les utilisateurs SSO : ```sql ALTER TABLE users ADD COLUMN auth_method TEXT DEFAULT 'local'; -- Valeurs: 'local', 'gitea', 'github', 'saml', 'oidc' ``` --- ## 8. Tests | Test | Description | Outil | |------|-------------|-------| | SAML login flow | Redirection complète IdP → callback → session | Playwright + mock IdP | | SAML metadata | Endpoint retourne XML valide | Unit test | | SAML assertion validation | Signature invalide → rejet | Unit test | | SAML replay attack | Même assertion utilisée 2× → rejet | Unit test | | OIDC login flow | Authorization code → token → session | Playwright + mock OIDC | | Auto-provision | Nouveau user SSO → création auto | Unit test | | Group mapping | Groupes SSO → rôles workspace | Unit test | | SSO disable | Désactiver SSO → tous les users restants gardent accès | Unit test | | SLO | Logout → redirection IdP SLO | Playwright | | Mixed auth | Login SSO + login local dans le même workspace | Integration test | --- ## 9. Checklist d'implémentation > ✅ **Tout est livré en v6.7.0** (`tests/test_v67_sso.py`, 38 tests) : 1. ✅ **`auth/providers/saml_provider.py`** — wrapper python3-saml (`app/auth/providers/saml_provider.py`) 2. ✅ **`auth/providers/oidc_provider.py`** — wrapper OIDC authlib (`app/auth/providers/oidc_provider.py`) 3. ✅ **`routers/sso.py`** — endpoints SSO/OIDC (`app/routers/sso.py` + API admin `/api/v2/sso/*`) 4. ✅ **Migration 23 `sso_config`** + `sso_login_history` + `sso_requests` + colonne `auth_method` sur `users` (déjà présente) 5. ✅ **`services/sso_provisioning.py`** — auto-provision + group mapping 6. ✅ **Extension `settings.html`** — UI admin SSO (onglet « SSO / Enterprise ») 7. ✅ **Extension page de login** — boutons SSO (nom dynamique) 8. ✅ **SP metadata endpoint** (`/auth/saml/metadata`) 9. ✅ **Security** — validation assertions (sign/aud/dest/InResponseTo), anti-replay `sso_requests`, rate limit login, historique 10. ✅ **Tests** — `tests/test_v67_sso.py` : 38 scénarios (flots SAML/OIDC, négatifs, groupes, sso_only, SLO) 11. ✅ **Documentation utilisateur** — `/help` section « SSO (Enterprise) » 12. ✅ **Dépendance** — `python3-saml==1.16.0`, `authlib==1.8.0`, `cryptography>=42.0` dans requirements.txt --- ## 10. Dépendances Python ``` # requirements.txt additions pour v6.0.0 SSO python3-saml>=1.16.0 # SAML 2.0 SP authlib>=1.3.0 # OIDC client cryptography>=42.0 # Signature/encryption xmlsec>=1.3.0 # XML signature validation (optionnel) ``` --- ## 11. Références - [OASIS SAML 2.0 Core Specification](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf) - [SAML 2.0 for Dummies (Simplified)](https://www.onelogin.com/sites/default/files/resources/SAML_2.0_for_Dummies.pdf) - [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) - [Azure AD SAML integration](https://learn.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-saml-idp) - [Authlib — OIDC client](https://docs.authlib.org/en/latest/client/oidc.html) - `app/auth/providers.py` — Architecture provider existante - `app/services/permission_manager.py` — Extension des rôles - `docs/API_GUIDE_V6.md` — Référence API v2 - `ROADMAP.md` — v6.0.0 SSO/SAML item