ADR-0015 — SSO interne par échange explicite (portail), pas de token en URL¶
Date : 2026-06-17 (clarifiée 2026-06-21)
Catégorie : A (convention de sécurité transverse, cadre auth déjà validé)
Décideur : Lead architecte DYNORS
Statut : Acceptée (cible) — implémentation par paliers ; implémentation autoritaire = dynors-platform/sly-portal-service (clarification 2026-06-21)
S'appuie sur : [[0011-jwt-audience-realm-scoping]] (aud par app), [[0013-jwt-asymetrique-kid-jwks-rotation]] (RS256/JWKS),
[[0014-cookies-isolation-par-app-pas-de-sso-implicite]] (cookies host-only, pas de SSO implicite)
Sources : docs/projet-nouveau/SlyPortal/AUDIT_SECURITE_ECHANGES_PORTAL.md ; prototype historique claude-handoff-dynors/sly-portal/ (à archiver — voir §Implémentation autoritaire)
⚠️ Implémentation autoritaire — résolution duplication (clarification 2026-06-21)¶
Deux arborescences de code coexistaient — confusion source d'erreurs et de double maintenance :
| Emplacement | Nature | Décision 2026-06-21 |
|---|---|---|
dynors-platform/sly-portal-service/ |
Module production complet : SlyPortalApplication, PortalTokenService, HandshakeExchangeService, PortalLaunchController, PortalExchangeController, PortalLogoutController, PortalMeController, PortalTokenBlacklist, ServiceApiKeyFilter, SlyPortalSecurityConfig, SlyPortalProperties, PortalUserSession, PortalSessionLoader |
Source de vérité autoritaire — toute évolution se fait ici |
claude-handoff-dynors/sly-portal/ |
Prototype historique (sous-jacent de l'audit AUDIT_SECURITE_ECHANGES_PORTAL.md) — contient encore l'ancien flow ?sly_token= en URL identifié comme faille critique |
Référence de migration uniquement — ne pas faire évoluer ; à archiver une fois la migration vérifiée |
Règle de revue : tout PR sur claude-handoff-dynors/sly-portal/ est refusé (sauf documentation explicative). Tout nouveau composant SSO va dans dynors-platform/sly-portal-service/.
Reste à faire pour clôturer la migration :
| Item | Description | Owner |
|---|---|---|
| PORT-MIG-001 | Audit code-à-code handoff → sly-portal-service pour vérifier que toutes les corrections de l'audit sont présentes dans la version production : Correction #1 cookie de handshake host-only à portée restreinte ; #2/#5 back-channel via InterAppCallService + transit HMAC (ADR-0008) ; #3 code opaque single-use Redis TTL ≤ 60 s ; #4 /portal/launch en POST + anti-CSRF ; #6 secret de signature en Vault ; cookies portail Secure + HttpOnly + SameSite |
Équipe plateforme — audit livré 2026-06-30 dans docs/projet-nouveau/SlyPortal/PORT_MIG_AUDIT_2026-06-30.md : 3 corrections critiques (#1, #2/#5, #3) conformes code-à-code ; 3 autres (#4 CSRF, #6 Vault, cookies session) à valider côté configuration prod |
| PORT-MIG-002 | Documenter dans dynors-platform/sly-portal-service/README.md la correspondance avec le prototype et les écarts conscients (avec lien vers l'audit) |
Équipe plateforme — section prête à coller dans le rapport PORT-MIG audit §PORT-MIG-002 |
| PORT-MIG-003 | Renommer claude-handoff-dynors/sly-portal/ en claude-handoff-dynors/sly-portal.archive/ avec NOTICE.md pointant vers le module production |
Équipe plateforme (après PORT-MIG-001) — commande prête dans le rapport §PORT-MIG-003 |
| PORT-MIG-004 | Aligner avec ADR-0013 palier 2 (dynors-auth JWKS) : à terme dynors-auth émet le token cible (pas le portail directement) — couplage propre quand le palier 2 sera livré |
Équipe plateforme (dépend palier 2 ADR-0013) — plan d'intégration dans le rapport §PORT-MIG-004 |
Contexte¶
ADR-0014 a tranché : aucun cookie partagé entre apps, chaque app a son cookie host-only et son token
(aud propre). Le confort d'un SSO interne (ne pas ressaisir ses identifiants en passant d'une app
DYNORS à une autre) doit donc se faire par un échange explicite, pas par propagation d'un cookie de
domaine parent.
Un prototype de portail existe (sly-portal) : session portail → bouton « lancer app » → génération d'un
sly_token (JWT HS256, TTL 5 min, jti usage unique via Redis) → redirection. L'audit a relevé une
faille critique : le sly_token transite dans l'URL (?sly_token=…) — fuite via logs serveur,
Referer, historique navigateur, APM. Le moteur (TTL court, single-use, blacklist) est sain ; le
transport et l'alignement avec ADR-0011/0013/0014 ne le sont pas.
Décision¶
SSO interne = flow d'échange type « authorization code », jamais de token dans l'URL ni de cookie partagé.
-
Session au portail uniquement. Le portail (
portal.dynors.comvia SLY) détient la session humaine dans son propre cookie host-only (__Host-sid, ADR-0014). Aucune autre app ne voit ce cookie. -
Code d'échange opaque, à usage unique. Au lancement d'une app, le portail émet un code opaque aléatoire (pas un JWT lisible), stocké côté serveur (Redis), TTL court (≤ 60 s), lié à
{userId, tenant, app(tech code), front, env, jti}, single-use. Le navigateur ne reçoit que cecode— il ne révèle rien et est inutile après un échange. -
Transport sans fuite. Le code ne transite pas en query string. Transport retenu : cookie de handshake host-only à portée restreinte posé par le portail puis redirection vers une URL propre
/{app}/{front}/login(SLY reverse-proxy, même origine) — cf. audit Correction #1. Variante acceptable : auto-POST. Jamais?code=ni?sly_token=dans une URL loggable. -
Échange back-channel (serveur-à-serveur). L'app cible appelle SLY via
InterAppCallService+ transit HMAC (ADR-0008), jamais en Feign/HTTP direct (audit Correction #2/#5) :POST /sly/portal/exchange{code}→ SLY valide (TTL, single-use, marquejticonsommé) et renvoie une identité minimale{sub, tenant, aud=<tech code app>}. Pas de rôles ni de permissions dans le handshake. -
L'app cible pose SON cookie + SON token. À partir de l'identité reçue, l'app obtient un token qui lui est propre (
aud= son tech code, ADR-0011 ; émis pardynors-authou localement en transitoire, RS256/JWKS ADR-0013) et le pose dans son cookie host-only__Host-sid(ADR-0014). Le SSO est « transparent » pour l'utilisateur, mais chaque app finit avec un token distinct et révocable. -
Autorisation = entitlements, pas le handshake. Le « qui peut quoi » reste runtime via
dynors-entitlements(séparation RBAC/entitlements, finding E2). Le handshake ne porte que l'identité. -
Durcissements (audit) :
/portal/launchen POST (effet de bord + anti-CSRF), cookies portailSecure + HttpOnly + SameSite, secret de signature en Vault (et migration HS256→RS256 cohérente ADR-0013), payload chiffré (JWE) en défense en profondeur si un JWT est conservé.
Conséquences¶
Positif : SSO sans cookie partagé ni confiance implicite ; rien d'exploitable ne transite côté navigateur
(code opaque, single-use, TTL ≤ 60 s) ; chaque app reste isolée (token aud propre, cookie host-only,
révocable indépendamment) ; surface minimale, cohérent avec ADR-0011/0013/0014.
Coût : un aller-retour back-channel de plus au lancement (négligeable) ; le portail doit tenir un store
de codes (Redis, déjà présent pour la blacklist jti) ; l'app cible doit exposer un /login qui consomme
le handshake et pose son cookie.
À reprendre dans le prototype sly-portal : remplacer sly_token en URL par le code/handshake (audit
Corr. #1) ; échange via InterAppCallService (Corr. #2/#5) ; retirer name/profile/permissions du jeton
transporté (identité minimale) ; aligner l'aud sur le tech code (layer 3) ; cookie final __Host-.
Alternatives écartées¶
sly_token(JWT) dans l'URL (prototype actuel) : fuite logs/Referer/historique → écartée (faille critique).- Cookie partagé
.dynors.com: déjà écartée par ADR-0014 (confiance implicite, rayon de souffle). - Handshake portant rôles/permissions : couple autz au transport → écartée (autz = entitlements, E2).
Références¶
- ADR-0011 (aud par app), ADR-0013 (asymétrique/JWKS), ADR-0014 (cookies host-only)
docs/projet-nouveau/SlyPortal/AUDIT_SECURITE_ECHANGES_PORTAL.md,.../PLAN_IMPLEMENTATION_POST_AUDIT.mddocs/security/SURFACE_ATTAQUE_FRONT_IDENTITE_2026-06-14.md,.cursor/rules/entitlements-auth-erreurs-conventions.mdc