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