Files
flowdeck/docs/V6_SSO_SAML_Enterprise_Auth.md

475 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<!-- 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 :
```xml
<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 :
```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