Aller au contenu

ADR-0002 — JWT minimal scopé par realm (aud), sans rôles ni produits dans le token

Date : 2026-05-07 (rédigée rétroactivement 2026-06-30 — backfill d'une décision déjà actée) Catégorie : A (convention d'identité transverse, dans le cadre de l'auth validée) Décideur : Lead architecte DYNORS Statut : Acceptée (implémentée — dynors-auth MVP, dynors-security JwtAuthFilter) S'appuie sur : [[0011-jwt-audience-realm-scoping]] (un aud par realm), [[0003-entitlements-source-agnostique-events]] (où vont les rôles), [[0013-jwt-asymetrique-kid-jwks-rotation]] (RS256/JWKS), [[0014-cookies-isolation-par-app-pas-de-sso-implicite]] (transport cookie) Source de vérité : dynors-docs/docs/05-architecture-si/dynors-auth.md §5 ; refactoring dynors-auth du 2026-05-07


Contexte

Le réflexe classique est d'embarquer les rôles et les produits accessibles dans le JWT : un seul token porte « qui je suis » et « ce que je peux faire ». C'est pratique (une lecture, zéro appel réseau) mais cela crée une fenêtre dangereuse : un droit change (changement de poste, résiliation, suspension pour impayé, retrait de rôle), or le token reste valide jusqu'à son expiration. L'utilisateur conserve alors des droits qu'il n'a plus, pendant 15 à 60 minutes.

DYNORS commercialise selon quatre modèles distincts (B2B SaaS récurrent, B2B onboardé, B2C transactionnel, grant administratif) et opère dans un contexte régulé (banque/assurance) où une révocation doit prendre effet en quelques secondes. Un JWT à rôles est incompatible avec cette exigence. Il faut séparer authentification (identité, lente à changer) et autorisation (droits, qui doivent pouvoir changer immédiatement).

Décision

  1. Le JWT émis par dynors-auth est minimal. Il porte exactement : iss (toujours dynors-auth), sub (UUID du sujet), tenant (code tenant, null pour B2C pur), aud (le realm), exp/iat. Rien d'autre.

  2. aud = realm, parmi quatre valeurs fermées : b2b (employé d'un tenant), b2c-<app> (client final d'un produit), internal (employé DYNORS via LDAP), tracking (accès anonyme éphémère). Chaque realm a une durée de vie courte (15-60 min selon le realm).

  3. Pas de roles ni de produits dans le token. Toute permission, tout rôle, tout produit accessible se résout à la volée via dynors-entitlements ([[0003-entitlements-source-agnostique-events]]), avec un cache 60 s côté apps. GET /auth/me retourne l'identité ; GET /entitlements/me retourne les capacités.

  4. Pas de source-app dans le JWT utilisateur. source-app est un concept service-to-service porté par InterAppCallService dans le header X-Source-App. Le mélanger au JWT crée des failles d'attribution et des couplages de déploiement incorrects.

Conséquences

Positif : révocation d'un droit effective en ~5 s (pas d'attente d'expiration du token) ; token léger et stable ; les contrats commerciaux changent sans toucher à l'auth ; séparation nette émission (dynors-auth) / validation (dynors-security) / autorisation (dynors-entitlements).

Coût : une app qui veut autoriser une action doit appeler dynors-entitlements (mitigé par le cache 60 s et l'event entitlement.invalidated qui purge à la révocation). Un appel réseau de plus sur le chemin d'autorisation, assumé pour la fraîcheur des droits.

Composants impactés : dynors-auth (émission), dynors-security JwtAuthFilter (validation signature + issuer + aud, alimente TenantContext, ne charge pas les droits), toutes les apps métier (consomment dynors-entitlements).

Alternatives écartées

  • Rôles dans le JWT : performant mais fenêtre de révocation = durée du token. Inacceptable en contexte régulé. Écartée.
  • JWT long + introspection systématique : revient à interroger un service à chaque requête tout en gardant un token lourd — cumule les inconvénients. Écartée au profit du JWT minimal + cache court.

Références

  • dynors-docs/docs/05-architecture-si/dynors-auth.md §5 (structure JWT, realms)
  • dynors-docs/docs/05-architecture-si/dynors-entitlements.md §7 (séparation auth/autz)
  • ADR-0011 (audience per-app), ADR-0013 (asymétrique), ADR-0014 (cookies host-only)