mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-28 01:53:40 +02:00
docs(knowledge): capitalisation backend — intégration du triage local (mai-juin 2026)
Triage et intégration des propositions backend du buffer 95_a_capitaliser.md (lot local RL799_V2 + app-alexandrie, mai-juin 2026), distinct de la capitalisation remote antérieure (triage 2026-05-02). ~73 entrées intégrées sur knowledge/backend/, dont : - patterns/auth.md : série "membrane d'auth fédérée BFF/OIDC" (9 patterns) + jose algo whitelist - patterns/prisma.md : recette fusionnée "Migration String/Int → enum" (backfill + Cas A/B/C), row réactivable, endpoint replace atomique, updateMany conditionnel, etc. - risques/general.md : 19 risques (epoch s vs ms, keepAliveTimeout=0, upsert+filtre liste, fail-safe catch-all, retrait asymétrique front/back, anti-énumération rate-limit, etc.) - patterns/general, async, nestjs, contracts, tests + risques/auth, contracts, prisma, redis, stripe, tests - compléments d'entrées existantes (authorize-after-fetch, P3014, cursor opaque, DI swc, Stripe v20...) - README patterns/risques mis à jour Doublons internes corrigés en relecture (suppression-champ .map() → general seul ; e2e DB-based → tests.md seul). Doublons hors backend / entrées projet / rejets non intégrés. Source 95_a_capitaliser.md non purgée à ce stade (purge en fin de capitalisation complète). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -297,6 +297,15 @@ Le helper `requireRoleAccess` doit retourner :
|
||||
- [ ] 403 uniquement quand le token est valide mais le rôle insuffisant
|
||||
- [ ] Frontend redirige vers `/login` sur 401, affiche "accès refusé" sur 403
|
||||
|
||||
### Complément — 403 vs 404 sur ressource existante non autorisée (oracle d'existence)
|
||||
|
||||
Sur une ressource cloisonnée par appartenance (colonne, tenant, propriétaire), le choix 403 vs 404 est lui-même un risque d'énumération. Si l'ordre des contrôles est « ressource introuvable → 404 » PUIS « non autorisé → 403 », un attaquant distingue « la ressource existe mais pas pour moi » (403) de « n'existe pas » (404) → énumération.
|
||||
|
||||
- Pour les ressources cloisonnées par appartenance, renvoyer **404 uniforme** dans le chemin non-privilégié (introuvable ET hors-périmètre confondus).
|
||||
- Réserver le **403** aux cas où l'existence de la ressource est déjà publique.
|
||||
- Trade-off : erreurs client moins précises. Pertinence proportionnelle à la prévisibilité des IDs (négligeable sur UUID v4, réelle sur IDs séquentiels).
|
||||
- Cas vécu : consultation d'instruction hors-colonne RL799 → bascule 403 → 404.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-auth-dernier-admin-actif"></a>
|
||||
@@ -677,3 +686,282 @@ test('rejette un token signé avec un autre purpose', async () => {
|
||||
- Tokens d'accès aux ressources publiques limitées dans le temps
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-jose-whitelist-algorithme"></a>
|
||||
## Pattern : jose — whitelister explicitement l'algorithme (signature ET chiffrement)
|
||||
|
||||
- Objectif : empêcher les attaques de confusion d'algorithme en figeant l'algo accepté à la vérification/déchiffrement, et non en se fiant à ce que la clé permet.
|
||||
- Contexte : tout module qui vérifie (`jwtVerify`) ou déchiffre (`jwtDecrypt`) des tokens/sessions avec `jose` — OIDC, JWE de session BFF, tokens internes.
|
||||
- Quand l'utiliser : à chaque appel `jose` qui consomme un token signé ou chiffré.
|
||||
- Quand l'éviter : jamais sur une surface qui consomme des tokens externes ou attaquables.
|
||||
- Avantage :
|
||||
- bloque la confusion d'algorithme (un attaquant ne peut pas forcer un algo plus faible présent dans le JWKS ou supporté par la clé)
|
||||
- rend explicite le contrat cryptographique attendu de l'émetteur
|
||||
- Limites / vigilance :
|
||||
- l'`alg` est un contrôle à part entière — `iss`/`aud`/`exp` ne le couvrent pas
|
||||
- garder la whitelist synchronisée avec ce que l'IdP/l'émetteur produit réellement
|
||||
- Validé le : 13-06-2026
|
||||
- Contexte technique : jose / OIDC / JWE — RL799_V2
|
||||
|
||||
### Règle
|
||||
|
||||
`jwtVerify(token, jwks, { issuer, audience })` SANS `algorithms: [...]` accepte tout algo présent dans le JWKS → confusion d'algorithme. Idem `jwtDecrypt(jwe, key)` sans `keyManagementAlgorithms`/`contentEncryptionAlgorithms` accepte tout algo supporté par jose avec la clé fournie.
|
||||
|
||||
- À chaque `jwtVerify` : passer `algorithms: [<algo émis par l'IdP>]` (Keycloak = `['RS256']`).
|
||||
- À chaque `jwtDecrypt` : passer `keyManagementAlgorithms` + `contentEncryptionAlgorithms` figés sur ce que l'émetteur produit (ex. `['dir']` + `['A256GCM']`).
|
||||
- L'`alg` est le 4ᵉ contrôle obligatoire après `iss`/`aud`/`exp`. S'applique à tout (dé)chiffrement de tokens/sessions, pas seulement OIDC.
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] `algorithms: [...]` explicite sur chaque `jwtVerify`
|
||||
- [ ] `keyManagementAlgorithms` + `contentEncryptionAlgorithms` explicites sur chaque `jwtDecrypt`
|
||||
- [ ] Whitelist alignée sur l'émetteur réel
|
||||
- [ ] Test : un token signé/chiffré avec un autre algo est rejeté
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-membrane-auth-federee"></a>
|
||||
## Pattern : Membrane d'auth fédérée en coexistence — point de jonction unique
|
||||
|
||||
- Objectif : greffer un 2ᵉ système d'authentification (ex. Keycloak OIDC) à côté de l'auth maison sans la modifier, de façon rollback-safe et sans disperser l'AuthN/AuthZ.
|
||||
- Contexte : intégration d'un IdP fédéré en coexistence avec une auth existante, livrée par lots.
|
||||
- Quand l'utiliser : toute introduction d'un second mécanisme d'auth amené à coexister.
|
||||
- Quand l'éviter : remplacement immédiat sans phase de coexistence (cutover direct).
|
||||
- Avantage :
|
||||
- tout le code en aval ignore l'existence du 2ᵉ système (NFR « AuthN/AuthZ non dispersée »)
|
||||
- rollback par simple flag (zéro delta comportemental flag off)
|
||||
- invariant d'identité fédérée protège contre l'escalade de privilèges par claim forgé
|
||||
- Limites / vigilance :
|
||||
- la jonction devient async (chemin fédéré = déchiffrement + JWKS + DB) → propager `await` à tous les call sites
|
||||
- flag d'activation à lire à chaque requête, jamais memoizé
|
||||
- Validé le : 14-06-2026
|
||||
- Contexte technique : auth fédérée / OIDC / Keycloak BFF — RL799_V2
|
||||
|
||||
### Règles d'or
|
||||
|
||||
1. **Un seul point de jonction** : `resolveAuthPayload(request)` traduit token → contexte métier `{ userId, email, role, offices }`. Tout le reste du code ignore le 2ᵉ système.
|
||||
2. **Discrimination par SOURCE, jamais par inspection du contenu** : cookie/header A → chemin maison ; cookie B → chemin fédéré. Si les deux coexistent, le plus conservateur (maison) GAGNE.
|
||||
3. **Flag d'activation fail-closed STRICT** : ON ssi `process.env.FLAG === 'true'` (toute autre valeur = OFF), lu à CHAQUE requête (jamais memoizé — sinon les tests qui togglent via `vi.stubEnv` deviennent ordre-dépendants ; seule la CONFIG est lazy-memoizée).
|
||||
4. **Invariant sacré de l'identité fédérée** : le validateur de token ne sort QUE l'identité (`sub` + `email` informatif), JAMAIS un claim d'autorisation (`realm_access.roles`, grade, office). `role`/`offices` dérivés EXCLUSIVEMENT de la DB locale. Test d'invariant obligatoire : forger un token avec `realm_access.roles: ['admin']` et vérifier qu'il n'accorde RIEN (403 sur route admin).
|
||||
5. **Migration des call sites async** : si la jonction rend les helpers d'auth async, propager `await` à TOUS les call sites par codemod mécanique guidé par `tsc`, et prouver le zéro-delta par la suite complète flag-off verte.
|
||||
|
||||
- Cas vécu : RL799 K1.1 (`authHelpers.ts::resolveAuthPayload`, 140 call sites migrés, test:api flag off vert).
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-frontiere-session-bff-stateless"></a>
|
||||
## Pattern : Frontière de session BFF stateless — JWE `dir`+`A256GCM`, fail-closed
|
||||
|
||||
- Objectif : porter une session contenant des tokens (OIDC ou autres) dans un cookie httpOnly sans store serveur (pas de Redis/table).
|
||||
- Contexte : archi BFF où les tokens vivent côté serveur, chiffrés dans un cookie, jamais exposés au JS.
|
||||
- Quand l'utiliser : session stateless chiffrée portée par cookie.
|
||||
- Quand l'éviter : sessions à store serveur (Redis/table) déjà en place, ou besoin de révocation immédiate côté serveur.
|
||||
- Avantage :
|
||||
- équivalent iron-session sans dépendance supplémentaire si `jose` est déjà là
|
||||
- cookie altéré → échec de déchiffrement (AEAD), expiration vérifiée gratuitement par `jwtDecrypt`
|
||||
- frontière isolée : un seul module manipule les tokens bruts
|
||||
- Limites / vigilance :
|
||||
- taille du cookie à surveiller (< 4096 octets)
|
||||
- secret dédié durci, validé strictement
|
||||
- Validé le : 14-06-2026
|
||||
- Contexte technique : jose / cookie httpOnly / BFF — RL799_V2
|
||||
|
||||
### Règles d'or
|
||||
|
||||
1. **Un SEUL module** (dé)chiffre et manipule les tokens bruts — vérifiable par `grep "from 'jose'"` limité à ce module + le validateur. Tout le reste reçoit du déjà-déchiffré.
|
||||
2. **JWE `alg: 'dir'` + `enc: 'A256GCM'`** (AEAD) avec `setIssuedAt`/`setExpirationTime` → l'`exp` du JWE donne l'expiration de session.
|
||||
3. **Whitelist explicite des algos au déchiffrement** (`keyManagementAlgorithms: ['dir']`, `contentEncryptionAlgorithms: ['A256GCM']`) — cf. [pattern jose — whitelist d'algorithme](#pattern-jose-whitelist-algorithme).
|
||||
4. **Secret DÉDIÉ par usage** (jamais le secret de chiffrement-au-repos), validé `/^[0-9a-f]{64}$/i` — PAS seulement `length === 64` (64 chars non-hex → Buffer < 32 bytes → erreur cryptique ; `A256GCM dir` exige exactement 32 bytes). Fail-fast avec hint de génération, lu lazy NON memoizé (testabilité).
|
||||
5. **Fail-closed asymétrique** : la clé est obtenue AVANT le try/catch (secret absent/malformé = erreur OPS → doit TRAVERSER → 500), MAIS toute erreur de déchiffrement/format/exp dans le try → `null` (cookie invalide, pas une panne) + log du TYPE d'échec (`decrypt_failed|expired|malformed`) SANS le contenu.
|
||||
6. **La frontière est une LIB** : elle ne fabrique jamais de `Response` — la couche jonction traduit `null` en 401.
|
||||
7. Logger via le logger structuré du projet (pino), pas `console.*`.
|
||||
|
||||
- Cas vécu : RL799 K1.2 `lib/keycloak/session.ts` (cookie ~3 ko < 4096, TTL 8 h aligné Max-Age + exp JWE).
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-pont-identite-federee"></a>
|
||||
## Pattern : Pont d'identité fédérée — colonne `idpSub` nullable + finder fail-safe
|
||||
|
||||
- Objectif : relier un identifiant opaque d'IdP (le `sub` OIDC) à un `User` local, en posant d'abord l'interface (stub) puis la résolution réelle quand le découpage en lots l'impose.
|
||||
- Contexte : série « membrane d'auth fédérée » — liaison `sub IdP → User` local sans toucher la couche consommatrice.
|
||||
- Quand l'utiliser : besoin d'un mapping identité externe → user local, livré par lots.
|
||||
- Quand l'éviter : pas de fédération d'identité, ou liaison déjà existante.
|
||||
- Avantage :
|
||||
- migration purement additive, back-fill incrémental possible
|
||||
- shape figé par la jonction → toute divergence casse la jonction au typecheck
|
||||
- aucune fuite de l'`idpSub` (clé de liaison interne)
|
||||
- Limites / vigilance :
|
||||
- colonne nullable tant que le cutover n'est pas complet (NOT NULL seulement après)
|
||||
- le finder ne décide pas de l'accès — il remonte l'état brut
|
||||
- Validé le : 14-06-2026
|
||||
- Contexte technique : Prisma / OIDC / repository — RL799_V2
|
||||
|
||||
### Règles d'or
|
||||
|
||||
1. **Migration purement additive** : `ADD COLUMN <sub> TEXT` + `CREATE UNIQUE INDEX`, **nullable, pas de NOT NULL, pas de DEFAULT, pas de back-fill**. Les comptes pré-fédération restent NULL (Postgres tolère plusieurs NULL sous un index `@unique` → back-fill incrémental). Le NOT NULL viendrait seulement après cutover complet.
|
||||
2. **Le finder vit au repository** (`route → service → repository`), retourne EXACTEMENT le shape figé par la couche jonction (ex. `{ id, email, role, isActive }`), via `select` minimal — jamais le mapper complet du record.
|
||||
3. **Fail-safe** : try/catch → `null` (une panne DB ne doit pas 500 le chemin authentifié).
|
||||
4. **Ne filtre PAS sur l'état actif** : retourne `isActive` tel quel — la décision d'accepter/rejeter appartient à la jonction (source unique).
|
||||
5. **Sens d'import** : la lib (subResolver) importe la VALEUR du finder ; le repository importe le TYPE du contrat en `import type` (effacé à la compilation → pas de cycle runtime).
|
||||
6. **No-leak** : l'`idpSub` est une clé de liaison interne — jamais en réponse API, jamais loggé. Le `userId` exposé en aval est l'`id` LOCAL, jamais le `sub`.
|
||||
7. **Prouver la levée du stub par un test E2E qui n'injecte PAS** le resolver de test (laisse le resolver de prod réel) : poser un `sub` sur un user de seed, forger un token, flag on → résolution DB réelle. Sans ce test, on prouve seulement le finder, pas le câblage.
|
||||
|
||||
- Cas vécu : RL799 K1.3 `findUserByKeycloakSub` + `User.keycloakSub`.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-greffe-effet-de-bord-regalien"></a>
|
||||
## Pattern : Greffe d'effet de bord externe best-effort sur une opération régalienne
|
||||
|
||||
- Objectif : déclencher un effet de bord EXTERNE non-critique (provisionner une identité IdP, notifier un tiers) depuis une mutation locale CRITIQUE sans jamais coupler leur succès.
|
||||
- Contexte : mutation régalienne (créer un membre) qui doit déclencher un appel externe non-bloquant.
|
||||
- Quand l'utiliser : tout effet de bord externe non-critique greffé sur une opération locale qui ne doit jamais échouer.
|
||||
- Quand l'éviter : effet de bord faisant partie de la transaction logique (audit régalien — cf. [pattern audit best-effort vs régalien](#pattern-auth-audit-best-effort-vs-regalien)).
|
||||
- Avantage :
|
||||
- l'opération locale réussit même si l'effet de bord échoue
|
||||
- reprovisionnable au rejeu (idempotent), zéro doublon
|
||||
- rollback par flag (zéro appel réseau flag off)
|
||||
- Limites / vigilance :
|
||||
- timeout interne obligatoire pour ne pas retarder la réponse régalienne
|
||||
- service account dédié à périmètre minimal, secrets jamais loggés
|
||||
- Validé le : 15-06-2026
|
||||
- Contexte technique : auth fédérée / provisioning IdP / fetch — RL799_V2
|
||||
|
||||
### Règles d'or
|
||||
|
||||
1. **Architecture en couches stricte** : `clientBas-niveau` (HTTP/fetch isolé, zéro logique métier) ← `serviceOrchestration` (chercher-avant-créer, fail-safe, écriture DB) ← greffe dans le service régalien. Jamais d'appel HTTP direct depuis le service métier.
|
||||
2. **Fail-safe TOTAL au service** : try/catch englobant → retourne `null` (jamais de throw vers l'appelant régalien), log + audit du statut. L'entité « non-provisionnée » est reprovisionnable au rejeu.
|
||||
3. **Idempotent par chercher-avant-créer** : interroger l'externe par clé naturelle (email `exact=true`) avant de créer ; réutiliser l'existant.
|
||||
4. **Timeout interne obligatoire** (`AbortController`, ex. 5 s) : un externe qui pend ne doit pas bloquer la réponse régalienne. Placer la greffe APRÈS les autres effets de bord prioritaires (ex. envoi de mail).
|
||||
5. **Token machine-to-machine memoizé avec marge d'expiration** (client credentials), pas un fetch par appel. Service account DÉDIÉ à périmètre minimal (`manage-users`), distinct du client d'auth utilisateur.
|
||||
6. **Minimisation des données** vers l'externe : body strictement limité à l'identité d'auth, zéro donnée métier sensible (invariant RGPD). Tester par assertion sur les clés exactes du payload.
|
||||
7. **Flag-gated `=== 'true'` lu à chaque requête** : flag off = zéro appel réseau, zéro branche, zéro delta.
|
||||
8. **Secrets jamais loggés** (token, client_secret) ; ne logger que des identifiants non-sensibles (email, userId, statut).
|
||||
|
||||
- Cas vécu : RL799 K1.4 `provisionMemberIdentity` greffé sur `handleCreateMember`, fetch + token injectables pour tests (aucun IdP vivant en CI).
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-refresh-session-bff-serveur"></a>
|
||||
## Pattern : Refresh de session BFF 100 % serveur — signal `Set-Cookie` remonté à la frontière
|
||||
|
||||
- Objectif : rafraîchir l'access token expiré côté serveur, dans le point de jonction d'auth, sans que la couche LIB (sans accès à la `Response`) ne fabrique elle-même le cookie.
|
||||
- Contexte : archi BFF où les tokens OIDC sont chiffrés dans un cookie httpOnly, jamais exposés au JS.
|
||||
- Quand l'utiliser : refresh d'une session BFF stateless portée par cookie.
|
||||
- Quand l'éviter : session à store serveur où la rotation se fait côté store.
|
||||
- Avantage :
|
||||
- la jonction reste une LIB (pas de fabrication de `Response`)
|
||||
- re-login transparent côté front quand le SSO IdP est encore actif
|
||||
- aucun token exposé au JS
|
||||
- Limites / vigilance :
|
||||
- la réécriture du cookie doit être généralisée (cf. risque rotation refresh token IdP)
|
||||
- convention d'unité epoch en SECONDES (OIDC) au recalcul de `expiresAt`
|
||||
- Validé le : 15-06-2026
|
||||
- Contexte technique : openid-client / cookie httpOnly / BFF — RL799_V2
|
||||
|
||||
### Règles d'or
|
||||
|
||||
1. **La jonction remonte un champ `sessionCookieToApply?: string`** dans son contexte de retour — le `Set-Cookie` du nouveau cookie chiffré, calculé après refresh réussi. La LIB ne fabrique pas de cookie, elle REMONTE un signal.
|
||||
2. **Un handler de frontière l'attache** (`headers.append('Set-Cookie', ...)`). Point d'application : le handler d'HYDRATATION systématique post-login (`/me` / `/session`) appelé à chaque boot par les router guards.
|
||||
3. **Marge de refresh anticipé** (~30 s) : rafraîchir un token qui expire dans < marge, pour couvrir le temps de traitement + dérive d'horloge.
|
||||
4. **Fail-closed** : le helper de refresh retourne `null` sur tout échec (refresh token mort/révoqué, panne IdP), jamais de throw. La jonction mappe `null` → 401 code distinct (`SESSION_EXPIRED`) + purge du cookie zombie (clear immédiat, sinon le cookie mort reboucle).
|
||||
5. **Convention d'unité epoch en SECONDES** (OIDC) au recalcul de `expiresAt` post-refresh — un mélange s/ms est un bug classique coûteux.
|
||||
6. **Invariant minimisation** : après refresh, repasser par le validateur de token qui n'extrait QUE l'identité (sub/email). `role`/`permissions` viennent TOUJOURS de la DB.
|
||||
7. **Distinguer deux codes 401** : « access token expiré mais rattrapable par refresh » (transparent, pas remonté au front) vs « session non rafraîchissable » (`SESSION_EXPIRED` → re-login transparent côté front).
|
||||
8. **Hook de test injectable du grant** qui ENCAPSULE la résolution de config (discovery réseau) côté prod : un grant de test ne touche jamais le réseau. Tester sur le CODE DE PRODUCTION réel (jonction + handler `/me` + tokens RS256 forgés), pas sur des mocks.
|
||||
|
||||
- Cas vécu : RL799 K1.5 `refreshKeycloakSession` + `resolveAuthPayload` + `sessionCookieToApply` consommé par `handleGetSession`, openid-client v6 `refreshTokenGrant`.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-cutover-auth-irreversible"></a>
|
||||
## Pattern : Cutover d'auth irréversible — isoler le point de non-retour hors du chemin auto
|
||||
|
||||
- Objectif : livrer une bascule destructive (retrait d'un système d'auth legacy au profit d'un nouveau) en gardant tout réversible sauf les 2-3 actions vraiment irréversibles, isolées derrière une gate et un prérequis ops.
|
||||
- Contexte : migration destructive — retrait d'un système legacy, `ALTER COLUMN SET NOT NULL` après backfill, `DROP TABLE`.
|
||||
- Quand l'utiliser : toute migration où le rollback cesse d'être possible une fois exécutée.
|
||||
- Quand l'éviter : changements purement additifs et réversibles.
|
||||
- Avantage :
|
||||
- le code livré reste réversible via git tant que l'irréversible n'est pas exécuté
|
||||
- aucun `migrate deploy` ne franchit le point de non-retour par accident
|
||||
- gate GO/NO-GO + runbook documenté
|
||||
- Limites / vigilance :
|
||||
- prérequis ops hors-code = bloquant explicite, story marquée NON-MERGEABLE si non vérifiable par l'agent
|
||||
- vérifier en review les 3 risques d'un retrait d'auth
|
||||
- Validé le : 15-06-2026
|
||||
- Contexte technique : migration destructive / Prisma / cutover — RL799_V2
|
||||
|
||||
### Règles d'or
|
||||
|
||||
1. **Séparer le réversible de l'irréversible.** Tout le code (retrait des branches legacy, nouveaux chemins) est livré et vert — réversible via git. Les 2-3 actions VRAIMENT irréversibles (migration `NOT NULL`, `DROP TABLE`) sont placées en **migration DRAFT HORS du dossier que l'outil applique automatiquement** (ex. `docs/infra/cutover-migration-draft/migration.sql`, PAS `prisma/migrations/`). Le schéma déclaré reste réversible (`String?`).
|
||||
2. **Gate bloquante AVANT l'irréversible** : un script lecture-seule (`cutover-check`) qui vérifie les pré-conditions (0 orphelin cassé par la contrainte) et émet GO/NO-GO. Ordre IMPOSÉ et documenté en runbook : prérequis ops → gate verte → migration → retrait final. Jamais inverser.
|
||||
3. **Prérequis ops hors-code = bloquant documenté** : si la bascule dépend d'une capacité externe (config realm IdP, SMTP, service account) que l'agent NE PEUT PAS vérifier, marquer la story explicitement NON-MERGEABLE et le tracer. Ne pas présumer « code vert = prêt à merger ».
|
||||
4. **Vérifier en review les 3 risques d'un retrait d'auth** : (a) tout hook/scaffolding de test sur le chemin d'auth est STRUCTURELLEMENT inerte en prod (call-site unique dans le setup de test, `null` au module-level, chargé seulement par le runner — grep exhaustif des call-sites) ; (b) retrait NET prouvé par grep 0-hit des primitives legacy (`createToken|verifyToken|verifyPassword`) en code de prod ; (c) le point de non-retour est bien hors du chemin auto.
|
||||
|
||||
- Cas vécu : RL799 K1.8 cutover Keycloak — migration NOT NULL+DROP en draft hors-Prisma, gate `runCutoverGate`, prérequis FR10 realm bloquant, hook resolver de test inerte vérifié.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-surface-publique-token-opaque"></a>
|
||||
## Pattern : Surface publique authentifiée par token opaque (quick-link mail, sans login)
|
||||
|
||||
- Objectif : exposer une surface publique non authentifiée par un token opaque (quick-link reçu par mail) en durcissant systématiquement contre l'énumération, le prefetch et la fuite de données.
|
||||
- Contexte : route accessible sans login via un token de mail (présence-sans-friction, instruction sans-friction, RSVP).
|
||||
- Quand l'utiliser : toute surface publique à token opaque.
|
||||
- Quand l'éviter : authentification membre classique — utiliser refresh + access httpOnly.
|
||||
- Avantage :
|
||||
- identité prouvée par possession du token, éligibilité re-vérifiée à chaque requête
|
||||
- pas d'oracle d'énumération (réponse neutre indistinguable)
|
||||
- GET read-only protège contre le prefetch (SafeLinks Outlook)
|
||||
- Limites / vigilance :
|
||||
- mapper de sortie dédié obligatoire (le mapper interne fuit des champs)
|
||||
- rate-limiter dédié à enregistrer dans le registre central de reset
|
||||
- Validé le : 23-06-2026
|
||||
- Contexte technique : surface publique / token opaque / mail — RL799_V2
|
||||
|
||||
### Checklist de durcissement (validée 2× sur RL799)
|
||||
|
||||
1. **Token opaque = identité, JAMAIS éligibilité.** Stocker `sha256(token)` en DB (jamais le clair), lookup par hash sur index unique. Le clair ne vit qu'en mémoire au dispatch.
|
||||
2. **Garde partagée GET+POST** (`resolveDeliveryAndGuards`) en union discriminée `{ ok: true, ... } | { ok: false, code }` — un seul point de revalidation appelé par les deux verbes (DRY + cohérence).
|
||||
3. **Re-valider l'éligibilité à CHAQUE requête** (membre actif + grade/colonne), pas seulement à l'émission du token — bloque la pollution après changement d'état.
|
||||
4. **401 NEUTRE indistinguable** : token forgé ET inéligibilité membre → MÊME code + MÊME message (constante `NEUTRAL_TOKEN_MESSAGE`). Pas d'oracle d'énumération RGPD. La distinction est tracée UNIQUEMENT côté serveur (`logSecurityEvent`). À TESTER : `expect(messageMismatch).toBe(messageForged)`.
|
||||
5. **Mapper de sortie DÉDIÉ minimal** — ne JAMAIS réutiliser le mapper interne (qui fuite `organizerId`/agrégat/id). À TESTER par clés EXACTES : `expect(Object.keys(data).sort()).toEqual([...])` + boucle sur les champs interdits.
|
||||
6. **GET strictement read-only** (anti-prefetch SafeLinks Outlook) ; mutation seulement au POST (clic explicite). À TESTER : un GET n'écrit rien en DB.
|
||||
7. **Rate-limiter IP dédié**, AJOUTÉ au registre central de reset (sinon flakiness inter-fichiers).
|
||||
8. **Domaine d'erreur dédié** (pas de réutilisation d'un code d'un autre domaine public).
|
||||
9. **Route fine** (export GET/POST → handler thin → service). Aucune logique dans la route.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-rbac-derive-source-unique"></a>
|
||||
## Pattern : RBAC dérivé d'une source de vérité — JWT enrichi + union au guard
|
||||
|
||||
- Objectif : dériver une autorité (rôle/capacité) d'une source unique (mandat, relation) sans la dupliquer ni introduire de fenêtre d'incohérence.
|
||||
- Contexte : un rôle/accès doit dériver d'une entité (ex. `OfficerMandate`) sans devenir une source concurrente.
|
||||
- Quand l'utiliser : autorité dérivable d'une relation, mutation de la source rare.
|
||||
- Quand l'éviter : besoin de fraîcheur immédiate (la source doit se propager en < TTL du token).
|
||||
- Avantage :
|
||||
- NFR « pas de fenêtre d'incohérence » satisfaite par construction (source lue, jamais dupliquée comme rôle séparé)
|
||||
- cumul des autorités préservé
|
||||
- reste zéro-DB au guard
|
||||
- Limites / vigilance :
|
||||
- un changement de la source met jusqu'à la durée de vie du token (ex. 15 min) à se propager — acceptable si la mutation est rare
|
||||
- Validé le : 12-06-2026
|
||||
- Contexte technique : auth / JWT / RBAC dérivé — RL799_V2
|
||||
|
||||
### Règle
|
||||
|
||||
NE PAS écrire le rôle dérivé dans la table user (duplication + fenêtre d'incohérence à gérer). À la place :
|
||||
1. calculer les attributs dérivés au login/refresh via un helper unique, les injecter dans le JWT à côté du rôle de base ;
|
||||
2. au guard, évaluer l'union `{rôle de base} ∪ {attributs dérivés}` — reste zéro-DB.
|
||||
|
||||
Trade-off assumé : un changement de la source met jusqu'à la durée de vie du token à se propager.
|
||||
|
||||
### Stratégie de migration sans coupure (app live)
|
||||
|
||||
Basculer par ADDITION : backfill source → enrichir le token en coexistence → guards en union sur-ensemble → réduire l'enum en DERNIER (le typecheck servant de filet d'exhaustivité).
|
||||
|
||||
- Cas vécu : refonte RBAC RL799 (offices dérivés des mandats).
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user