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¶
-
Le JWT émis par
dynors-authest minimal. Il porte exactement :iss(toujoursdynors-auth),sub(UUID du sujet),tenant(code tenant,nullpour B2C pur),aud(le realm),exp/iat. Rien d'autre. -
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). -
Pas de
rolesni de produits dans le token. Toute permission, tout rôle, tout produit accessible se résout à la volée viadynors-entitlements([[0003-entitlements-source-agnostique-events]]), avec un cache 60 s côté apps.GET /auth/meretourne l'identité ;GET /entitlements/meretourne les capacités. -
Pas de
source-appdans le JWT utilisateur.source-appest un concept service-to-service porté parInterAppCallServicedans le headerX-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)