Aller au contenu

ADR-0015 — SSO interne par échange explicite (portail), pas de token en URL

Date : 2026-06-17 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 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 claude-handoff-dynors/sly-portal/


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