Aller au contenu

ADR-0014 — Cookies isolés par app (host-only), token distinct par app, pas de SSO implicite

Date : 2026-06-17 Catégorie : A (convention de sécurité transverse, dans le cadre de l'auth déjà validée) Décideur : Lead architecte DYNORS Statut : Acceptée S'appuie sur : [[0011-jwt-audience-realm-scoping]] (un aud par app), [[0013-jwt-asymetrique-kid-jwks-rotation]] (RS256 + JWKS) Implémentation : - Source unique com.dynors.commons.web.DynorsAccessCookie (dynors-commons 1.0.9) — noms de base + dérivation des préfixes __Host-/__Secure-. Émetteur et valideur en dépendent (plus de constante recopiée). - dynors-platform/authAuthController (cookies via DynorsAccessCookie), CookieProductionReadinessValidator (fail-fast prod si cookie-domain posé) ; dépend maintenant de dynors-commons. - core/security 1.0.9 — JwtAuthFilter lit le token depuis le cookie host-only (SecurityProperties.JwtProperties.cookie-enabled|name|secure, effectiveCookieName() via DynorsAccessCookie), priorité au header Bearer. - SIRRAT Forge / CodexTAIL_AUTH_COOKIE_NAME (défaut sid, nom neutre anti-fingerprinting), TAIL_AUTH_COOKIE_SECURE, TAIL_AUTH_COOKIE_ENABLED (changeset 017-sirrat-codex-cookie-isolation).


Contexte

La question posée : un cookie de session posé sur .dynors.com est renvoyé par le navigateur à tous les sous-domaines (api.supergest.dynors.com, sly.dynors.com…). C'est la mécanique classique du SSO « cookie de domaine parent ». Pratique, mais cela revient à faire confiance implicitement à toutes les apps du domaine : le même cookie circule partout, le rayon de souffle d'un sous-domaine compromis (XSS, sous-domaine oublié, takeover) couvre toute la plateforme.

DYNORS a pour posture « la menace ne vient pas forcément de nous » et vise une surface minimale et indevinable. Partager un cookie entre apps contredit cette posture.

Décision

  1. Cookie host-only par app. En prod, l'access cookie est posé avec le préfixe __Host- (force Secure, Path=/, interdit l'attribut Domain → le cookie est lié à l'hôte exact et n'est jamais envoyé à un autre sous-domaine). Le refresh utilise __Secure- (son Path restreint /auth/refresh est incompatible avec __Host- qui exige Path=/). Aucun Domain n'est jamais posé ; dynors.auth.cookie-domain est interdit en prod (le validateur échoue au démarrage s'il est renseigné).

Le nom de base est neutre (sid / rid) : il ne révèle ni la marque (« dynors ») ni la nature (« token ») — réduit le fingerprinting et la reconnaissance passive. Identique pour toutes les apps (sans risque : cookies host-only, jamais partagés). Le nom n'est pas un secret ; la vraie barrière reste HttpOnly + Secure + SameSite + __Host- + validation aud.

  1. Un token distinct par app. Chaque token porte son propre aud (= tech code de l'app, ADR-0011). Même si un cookie « fuyait » vers un autre hôte, le token serait rejeté par la validation d'audience de l'app cible. Cookie ≠ autorisation : l'accès est scopé par aud.

  2. Tout le monde s'authentifie. Aucune app n'hérite d'une session « déjà ouverte ailleurs ». Chaque app authentifie l'utilisateur — LDAP (interne) ou auth simple (B2C/B2B) — et reçoit son token. Pas de propagation automatique.

  3. SSO interne = échange explicite (pas implicite). Pour les apps internes, un SSO est prévu mais via un portail (SLY Portal, PortalTokenService) qui détient la session sur son propre hôte et émet à la demande un token court scopé pour l'app cible (token exchange). Jamais un cookie de domaine parent partagé. Ainsi l'utilisateur ne ressaisit pas ses identifiants, mais chaque app reçoit un token qui lui est propre et révocable.

Conséquences

Positif : isolation maximale (un sous-domaine compromis ne livre pas les autres) ; révocation par app ; aucune confiance implicite ; cohérent avec aud per-app et la posture « surface minimale ».

Coût : pas de SSO « silencieux » par simple cookie ; le confort SSO interne passe par le portail (token exchange) à implémenter (palier 2). En dev local (http), les préfixes __Host-/__Secure- exigeant HTTPS sont désactivés (secure-cookie=false) → noms de cookies nus ; la vraie politique s'applique dès qu'on est en HTTPS (env/prod).

Répercuté ✅ : le lecteur (dynors-security JwtAuthFilter) et l'émetteur (dynors-auth) dérivent le nom du cookie de la même source DynorsAccessCookie — divergence de préfixe impossible. Le réglage commun secure est piloté côté Forge par TAIL_AUTH_COOKIE_SECURE.

Alternatives écartées

  • Cookie domaine-large .dynors.com + aud strict : SSO simple, mais rayon de souffle large et confiance implicite entre apps → contraire à la posture. Écartée.
  • Tout host-only sans aucun SSO : retenu comme base ; le confort SSO interne est ajouté par le portail (échange explicite), sans renoncer à l'isolation.

Références

  • docs/security/SURFACE_ATTAQUE_FRONT_IDENTITE_2026-06-14.md (§ cookies)
  • .cursor/rules/entitlements-auth-erreurs-conventions.mdc
  • ADR-0011 (audience per-app), ADR-0013 (asymétrique), ADR-0008 (transit SLY par app)