475 lines
22 KiB
Markdown
475 lines
22 KiB
Markdown
# 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
|