22 KiB
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→mainDé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
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) :
{
"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 :
{
"login": "sub",
"email": "email",
"full_name": "name",
"avatar_url": "picture"
}
3.3 Configuration .env (exemples)
# 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
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
RelayStatecontient 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_secretchiffré avecapp_secret_keyavant stockage
5.3 Table sso_login_history
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
<!-- Extension de login.html — boutons SSO -->
<div class="sso-providers" x-show="ssoProviders.length > 0">
<div class="sso-divider">ou continuez avec</div>
<template x-for="provider in ssoProviders">
<button @click="ssoLogin(provider.type)" class="sso-btn">
<span x-text="provider.icon"></span>
<span x-text="provider.name"></span>
</button>
</template>
</div>
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 :
<EntityDescriptor entityID="https://flowdeck.local/saml/metadata">
<SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<KeyInfo>...</KeyInfo>
</KeyDescriptor>
<AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://flowdeck.local/auth/saml/callback" />
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://flowdeck.local/auth/saml/logout" />
</SPSSODescriptor>
</EntityDescriptor>
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 :
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 :
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) :
- ✅
auth/providers/saml_provider.py— wrapper python3-saml (app/auth/providers/saml_provider.py) - ✅
auth/providers/oidc_provider.py— wrapper OIDC authlib (app/auth/providers/oidc_provider.py) - ✅
routers/sso.py— endpoints SSO/OIDC (app/routers/sso.py+ API admin/api/v2/sso/*) - ✅ Migration 23
sso_config+sso_login_history+sso_requests+ colonneauth_methodsurusers(déjà présente) - ✅
services/sso_provisioning.py— auto-provision + group mapping - ✅ Extension
settings.html— UI admin SSO (onglet « SSO / Enterprise ») - ✅ Extension page de login — boutons SSO (nom dynamique)
- ✅ SP metadata endpoint (
/auth/saml/metadata) - ✅ Security — validation assertions (sign/aud/dest/InResponseTo), anti-replay
sso_requests, rate limit login, historique - ✅ Tests —
tests/test_v67_sso.py: 38 scénarios (flots SAML/OIDC, négatifs, groupes, sso_only, SLO) - ✅ Documentation utilisateur —
/helpsection « SSO (Enterprise) » - ✅ Dépendance —
python3-saml==1.16.0,authlib==1.8.0,cryptography>=42.0dans 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
- SAML 2.0 for Dummies (Simplified)
- OpenID Connect Core 1.0
- Azure AD SAML integration
- Authlib — OIDC client
app/auth/providers.py— Architecture provider existanteapp/services/permission_manager.py— Extension des rôlesdocs/API_GUIDE_V6.md— Référence API v2ROADMAP.md— v6.0.0 SSO/SAML item