Aller au contenu

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é.

  1. Session au portail uniquement. Le portail (portal.dynors.com via SLY) détient la session humaine dans son propre cookie host-only (__Host-sid, ADR-0014). Aucune autre app ne voit ce cookie.

  2. 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 ce code — il ne révèle rien et est inutile après un échange.

  3. 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.

  4. É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, marque jti consommé) et renvoie une identité minimale {sub, tenant, aud=<tech code app>}. Pas de rôles ni de permissions dans le handshake.

  5. 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 par dynors-auth ou 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.

  6. 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é.

  7. Durcissements (audit) : /portal/launch en POST (effet de bord + anti-CSRF), cookies portail Secure + 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.md
  • docs/security/SURFACE_ATTAQUE_FRONT_IDENTITE_2026-06-14.md, .cursor/rules/entitlements-auth-erreurs-conventions.mdc