mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-05-18 08:18:15 +02:00
Triage du 95_a_capitaliser.md (~75 propositions) : - 60 entrées intégrées dans knowledge/ (backend, frontend, workflow) - 4 nouveaux fichiers : backend/patterns/tests.md, backend/risques/tests.md, frontend/patterns/general.md, workflow/patterns/general.md - 6 doublons rejetés - Mise à jour des READMEs index pour refléter les nouvelles entrées - 95_a_capitaliser.md restauré à sa structure initiale - 40_decisions_et_archi.md : décision mono-tenant déployable vs SaaS multi-tenant - 90_debug_et_postmortem.md : sub-agents Write indisponible, effet iceberg CI, prisma migrate diffs cosmétiques Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
461 lines
19 KiB
Markdown
461 lines
19 KiB
Markdown
---
|
|
title: Backend — Risques & vigilance : Auth
|
|
domain: backend
|
|
bucket: risques
|
|
tags: [auth, guards, request-user, sessions, admin]
|
|
applies_to: [implementation, review, debug]
|
|
severity: high
|
|
validated_on: 2026-04-07
|
|
source_projects: [app-alexandrie, RL799_V2]
|
|
---
|
|
|
|
# Backend — Risques & vigilance : Auth
|
|
|
|
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
|
|
|
|
---
|
|
|
|
<a id="risque-authn-authz-dispersee"></a>
|
|
## AuthN/AuthZ dispersée (contrôles d'accès au fil de l'eau)
|
|
|
|
### Risques
|
|
|
|
- Règles de permissions incohérentes selon endpoints
|
|
- Failles "oubliées" sur un endpoint secondaire
|
|
- Audit impossible
|
|
|
|
### Symptômes
|
|
|
|
- Utilisateurs qui accèdent à des ressources non prévues
|
|
- Correctifs en urgence "on ajoute un if ici"
|
|
- Bugs qui réapparaissent après refactor
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Centraliser authn/authz (middleware/policies)
|
|
- Tests sur règles critiques
|
|
- Logs/audit des décisions d'accès
|
|
|
|
---
|
|
|
|
<a id="risque-guard-global-manquant"></a>
|
|
## Guard global manquant (request.user jamais peuplé)
|
|
|
|
### Risques
|
|
|
|
- Chaîne auth bâtie sur une fondation inopérante (tout "a l'air OK" en dev/tests, mais casse en prod)
|
|
- Guards aval qui dépendent de `request.user` en erreur (ou contournements involontaires)
|
|
- Découvert tard (souvent uniquement en code review ou en prod)
|
|
|
|
### Symptômes
|
|
|
|
- `request.user` vaut `undefined` dans un guard supposé "après auth"
|
|
- Endpoints qui passent alors qu'ils devraient être refusés (si les guards aval se désactivent/retournent true par défaut)
|
|
- Tests "verts" car trop mockés (pas de test e2e qui valide le pipeline complet)
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Poser explicitement le guard global dès les foundations (au moins `AuthGuard`)
|
|
- Vérifier l'ordre des `APP_GUARD` (AuthGuard avant tout guard qui lit `request.user`)
|
|
- Ajouter au minimum 1 test d'intégration/e2e qui prouve que `request.user` est bien peuplé sur un endpoint protégé
|
|
|
|
---
|
|
|
|
<a id="risque-guard-request-user-null"></a>
|
|
## Guard NestJS route-level — null-check manquant sur `request.user`
|
|
|
|
### Risques
|
|
|
|
- Un guard route-level qui lit `request.user.userId` sans null-check lève une `TypeError` (500) si `request.user` est absent
|
|
- Mauvaise registration de module, test d'intégration mal configuré, ou middleware custom peuvent produire cet état
|
|
|
|
### Symptômes
|
|
|
|
- `TypeError: Cannot read properties of undefined (reading 'userId')` en prod
|
|
- Tests "verts" car `request.user` mocké globalement, mais pas le guard isolé
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
```typescript
|
|
const user = (request as any).user as { userId: string } | undefined;
|
|
if (!user?.userId) {
|
|
throw new UnauthorizedException({ error: { code: 'UNAUTHENTICATED', message: '...' } });
|
|
}
|
|
```
|
|
|
|
- **Règle** : les guards route-level ne font pas confiance aux guards globaux pour leurs invariants — ils se défendent eux-mêmes.
|
|
- Contexte technique : NestJS v10+ — 09-03-2026
|
|
|
|
---
|
|
|
|
<a id="risque-cookie-apres-revocation-db"></a>
|
|
## Suppression du cookie après révocation DB sur logout
|
|
|
|
### Risques
|
|
|
|
- Si la révocation DB échoue avant la suppression du cookie, l'utilisateur garde un cookie local devenu incohérent
|
|
- L'utilisateur peut rester bloqué dans un état où il ne peut plus se déconnecter proprement
|
|
- Le comportement diffère selon la disponibilité de la base
|
|
|
|
### Symptômes
|
|
|
|
- Logout qui échoue par intermittence quand la DB est instable
|
|
- Cookie de session toujours présent côté navigateur après erreur serveur
|
|
- Réessais de logout qui produisent des états difficiles à diagnostiquer
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Toujours supprimer le cookie en premier, même si la révocation DB échoue ensuite
|
|
- Traiter la suppression côté DB en best-effort ou avec gestion d'idempotence adaptée
|
|
- Vérifier en test qu'un échec DB ne laisse pas l'accès browser actif
|
|
- Contexte technique : Next.js / auth par cookie / session persistée — 16-03-2026
|
|
|
|
---
|
|
|
|
<a id="risque-get-sans-controle-acces"></a>
|
|
## Endpoints GET sans contrôle d'accès sur ressource protégée
|
|
|
|
### Risques
|
|
|
|
- Un endpoint de lecture expose des données premium/protégées à tout utilisateur authentifié
|
|
- La règle "seuls les writes vérifient les droits" est un anti-pattern qui cause des fuites silencieuses
|
|
|
|
### Symptômes
|
|
|
|
- `getCategories`, `getThreads` ou équivalent accessible sans vérification d'entitlements
|
|
- Endpoint write protégé par `assertForumAccess` mais GET correspondant non protégé
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Tout endpoint retournant des données liées à une ressource protégée (forum pack, contenu premium) doit appeler `assertForumAccess` ou équivalent, même pour les GET
|
|
- **Checklist review** : pour chaque nouveau GET, vérifier qu'il passe par le guard/helper d'accès si la ressource appartient à un scope protégé
|
|
|
|
- Contexte technique : NestJS / app-alexandrie — 23-03-2026
|
|
|
|
---
|
|
|
|
<a id="risque-adminroleguard-sans-decorateur"></a>
|
|
## NestJS `@UseGuards(AdminRoleGuard)` sans `@RequireAdminRole()` — silencieusement ouvert
|
|
|
|
### Risques
|
|
|
|
- `AdminRoleGuard.canActivate()` lit la metadata `REQUIRE_ADMIN_ROLE_KEY` posée par `@RequireAdminRole()`
|
|
- Si le décorateur est absent, `requiresAdmin = false/undefined` → le guard retourne `true` et laisse passer sans vérification
|
|
|
|
### Symptômes
|
|
|
|
- Endpoint admin accessible à tout utilisateur authentifié
|
|
- Zéro erreur de compilation ou de démarrage — le bug est silencieux
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
```typescript
|
|
// ✅ Correct — les deux décorateurs ensemble
|
|
@Post('admin/ressource')
|
|
@UseGuards(AdminRoleGuard)
|
|
@RequireAdminRole()
|
|
async createRessource(...) {}
|
|
|
|
// ❌ Silencieusement non protégé — @RequireAdminRole() manquant
|
|
@Post('admin/ressource')
|
|
@UseGuards(AdminRoleGuard)
|
|
async createRessource(...) {}
|
|
```
|
|
|
|
- Règle : s'applique à tout guard NestJS qui délègue la décision à une metadata de décorateur
|
|
- **Checklist review** : vérifier systématiquement les endpoints admin que `@RequireAdminRole()` est présent
|
|
|
|
- Contexte technique : NestJS / guards metadata — app-alexandrie 23-03-2026
|
|
|
|
---
|
|
|
|
<a id="risque-mock-session-sans-expiresat"></a>
|
|
## Mock Prisma session sans filtre `expiresAt` — divergence test/prod
|
|
|
|
### Risques
|
|
|
|
- Le mock `session.findFirst` omet de filtrer `expiresAt` → des sessions expirées passent en test alors qu'elles seraient rejetées en prod
|
|
- Masque des régressions sur la logique d'expiration de session
|
|
|
|
### Symptômes
|
|
|
|
- Tests e2e verts avec un token de session expiré
|
|
- Bug découvert uniquement en prod quand la TTL est dépassée
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
Le mock doit répliquer **tous** les critères de `getUserByToken()` en prod : `revokedAt === null` ET `expiresAt > now` :
|
|
|
|
```typescript
|
|
// ✅ Mock complet fidèle à la prod
|
|
findFirst: jest.fn().mockImplementation(({ where }) => {
|
|
const session = store[where.accessToken];
|
|
if (!session) return null;
|
|
if (where.revokedAt === null && session.revokedAt !== null) return null;
|
|
if (where.expiresAt?.gt && session.expiresAt <= where.expiresAt.gt) return null;
|
|
return session;
|
|
})
|
|
```
|
|
|
|
- **Règle** : `seedSession()` doit initialiser `expiresAt` à +30j par défaut. Ajouter un helper `seedExpiredSession()` si des tests de session expirée sont nécessaires.
|
|
|
|
- Contexte technique : NestJS / Prisma mock / e2e — app-alexandrie 24-03-2026
|
|
|
|
---
|
|
|
|
<a id="risque-tests-e2e-buildapp-partage"></a>
|
|
## Tests e2e autorisation : scénarios non-abonné avec `buildApp` partagé
|
|
|
|
### Risques
|
|
|
|
- Un `describe` e2e avec `buildApp` partagé en `beforeAll` (entitlements actifs) rend impossible le test de scénarios non-abonné sans pollution entre tests
|
|
- Tenter de surcharger le mock partagé (`jest.fn().mockResolvedValueOnce(...)`) dans un `it` intermédiaire est fragile et crée des effets de bord
|
|
|
|
### Symptômes
|
|
|
|
- Scénario "non-abonné → 403" n'est jamais testé, ou pollue les autres tests si le mock est modifié en cours de describe
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
Créer une instance `buildApp` isolée pour les scénarios d'autorisation alternatifs :
|
|
|
|
```typescript
|
|
it('retourne 403 si subscription inactive', async () => {
|
|
const isolatedApp = await buildApp({
|
|
getEntitlementsForUser: jest.fn().mockResolvedValue({
|
|
subscription: { isActive: false, plan: 'free' }
|
|
})
|
|
});
|
|
// ... tests
|
|
await isolatedApp.close();
|
|
});
|
|
```
|
|
|
|
- **Règle** : ne jamais tenter de surcharger un mock partagé dans un `it` — créer un `buildApp` isolé avec `app.close()` en fin de test
|
|
|
|
- Contexte technique : NestJS / Jest e2e — app-alexandrie 24-03-2026
|
|
|
|
---
|
|
|
|
<a id="risque-champ-metier-absent-jwt"></a>
|
|
## Champ métier absent du JWT — découplage silencieux frontend/backend
|
|
|
|
### Risques
|
|
|
|
- Le frontend lit un champ dans `decodeJwtPayload(token)` qui n'est jamais émis par le service d'authentification
|
|
- Le comportement est silencieux — `undefined` est traité comme `''`, aucune erreur visible, l'UI se dégrade sans signal d'alerte
|
|
|
|
### Symptômes
|
|
|
|
- `decodeJwtPayload(token).field` retourne `undefined` pour tous les utilisateurs réels
|
|
- Filtres de grade/rôle côté UI entièrement inopérants (rank=0, aucune tab affichée)
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Toute donnée lue via `decodeJwtPayload` côté frontend doit être explicitement émise dans le payload JWT côté backend
|
|
- Lors de l'ajout d'un champ à `JwtPayload` (type TypeScript), vérifier immédiatement que le service d'authentification inclut ce champ à l'émission
|
|
- Ajouter un test d'intégration login → decode qui vérifie la présence des champs critiques dans le token retourné
|
|
- **Signal review** : un champ apparaît dans le type `JwtPayload` côté frontend sans modification correspondante dans `authService.ts`
|
|
|
|
- Contexte technique : JWT / auth — RL799_V2 02-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-email-login-vs-contact-annuaire"></a>
|
|
## Confusion email login / email de contact dans un endpoint annuaire
|
|
|
|
### Risques
|
|
|
|
- Le mapping de l'endpoint annuaire utilise `email: user.email` (email de login, toujours présent) alors que l'intention est d'exposer un email de contact optionnel
|
|
- Même un utilisateur à bas privilège peut récupérer les emails de login de tous les membres
|
|
|
|
### Symptômes
|
|
|
|
- `email: user.email` dans le mapping d'un endpoint de type "annuaire" ou "liste membres"
|
|
- Emails de connexion exposés à tous les utilisateurs authentifiés
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Dans tout endpoint annuaire ou profil public, distinguer explicitement :
|
|
- **email de login** : identifiant de compte, JAMAIS exposé à un tiers dans un endpoint annuaire
|
|
- **email de contact** : champ optionnel dans le profil ou la table directory, exposé uniquement s'il est renseigné
|
|
- Si le modèle ne dispose pas encore d'un champ email de contact distinct, renvoyer `undefined` pour le champ email dans l'annuaire
|
|
- **Signal review** : `email: user.email` dans le mapping d'un endpoint annuaire
|
|
|
|
- Contexte technique : auth / annuaire — RL799_V2 02-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-toctou-rotation-refresh-token"></a>
|
|
## TOCTOU sur rotation de refresh token
|
|
|
|
### Risques
|
|
|
|
- Un pattern `findUnique` + `update` séparés sur la rotation de refresh token crée une fenêtre TOCTOU
|
|
- Deux requêtes concurrentes avec le même refresh token passent toutes les deux la vérification avant que l'une ne révoque → deux sessions valides émises, le vol de token passe inaperçu
|
|
|
|
### Symptômes
|
|
|
|
- Deux sessions actives issues du même refresh token
|
|
- Détection de vol impossible car les deux tokens sont valides
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Toujours utiliser un `updateMany` atomique avec condition `WHERE revokedAt IS NULL AND expiresAt > NOW()` et vérifier `count === 1`
|
|
- Si `count === 0`, le token a déjà été utilisé → révoquer **tous** les tokens du user (token family detection, RFC 6819)
|
|
- **Signal review** : `findUnique` suivi de `update` séparés dans un flux de rotation de refresh token
|
|
|
|
- Contexte technique : auth / refresh token — RL799_V2 08-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-drift-auth-copier-coller"></a>
|
|
## Drift d'authentification par copier-coller de pattern auth
|
|
|
|
### Risques
|
|
|
|
- Quand un helper d'auth centralisé existe (`requireRoleAccess`), mais que de nouveaux services réimplémentent le même pattern manuellement (`extractAccessToken` + `verifyToken` + vérification locale), chaque service développe ses propres variantes (codes d'erreur différents, 401 vs 403, requestId ou non)
|
|
- La surface d'auth devient incohérente et indéfendable en audit
|
|
|
|
### Symptômes
|
|
|
|
- Un audit RBAC révèle qu'une part significative des routes ont un pattern d'auth "fait maison" au lieu du helper standard
|
|
- Codes d'erreur divergents entre services pour la même situation (token absent)
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Tout nouveau handler HTTP DOIT utiliser le helper centralisé pour l'authentification et l'autorisation
|
|
- Ne JAMAIS importer `extractAccessToken` + `verifyToken` directement dans un service métier
|
|
- Si le helper ne couvre pas un besoin (ex: besoin de `userId` en plus de `email`), étendre le helper plutôt que contourner
|
|
- **Signal review** : import de `verifyToken` dans un fichier service (hors `authHelpers.ts`)
|
|
|
|
- Contexte technique : auth / architecture — RL799_V2 08-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-auth-acl-unique-champ-sensible"></a>
|
|
## ACL unique pour ressource globale et sous-champ sensible
|
|
|
|
### Risques
|
|
- Champs sensibles exposés à des rôles qui ne devraient accéder qu'à la vue agrégée.
|
|
|
|
### Symptômes
|
|
- Endpoint fonctionnellement "autorisé" mais fuite de notes/valeurs sensibles en clair.
|
|
|
|
### Bonnes pratiques / mitigations
|
|
- Séparer explicitement les règles d'accès : liste globale vs détails sensibles.
|
|
- Appliquer des guards dédiés au niveau du champ ou du sous-endpoint.
|
|
|
|
- Contexte technique : auth / ACL granulaire — RL799_V2 13-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-auth-jwt-user-introuvable"></a>
|
|
## JWT valide mais utilisateur introuvable en base
|
|
|
|
### Risques
|
|
- Retour `403` trompeur (authz) au lieu d'un `401` (auth invalide côté sujet).
|
|
|
|
### Symptômes
|
|
- Frontend affiche "accès refusé" au lieu de forcer une ré-authentification.
|
|
|
|
### Bonnes pratiques / mitigations
|
|
- Si le sujet JWT ne résout plus un user actif : répondre `401`.
|
|
- Déclencher invalidation de session côté client.
|
|
|
|
- Contexte technique : auth / cycle de vie compte — RL799_V2 17-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-helpers-x-actif-derivants"></a>
|
|
## Helpers "X actif" qui dérivent silencieusement
|
|
|
|
### Risques
|
|
|
|
- Plusieurs helpers répondent à la même question — *"l'entité X est-elle active / opérante ?"* — avec des filtres légèrement différents
|
|
- Un user passe la guard A mais pas la guard B sur la même ressource (ou inversement). Bugs silencieux, pas d'erreur, juste une asymétrie de comportement
|
|
|
|
### Symptômes
|
|
|
|
- Délégation `secretaireDeSeance` "active" filtrée sur `status: 'published', closedAt: null, cancelledAt: null` dans un helper, juste `cancelledAt: null` dans l'autre
|
|
- Un ex-délégué d'une soirée clôturée garde l'autorité cross-soirée indéfiniment
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
1. **Un seul helper canonique** par notion d'activité (ex : `isDelegationActive`, `isSoireeOpenForRappel`). Les autres l'appellent
|
|
2. Si la centralisation n'est pas faisable immédiatement (ex : helper appelé en N+1 query, perf), au moins un test qui compare leur output sur des fixtures partagées et casse à la moindre divergence
|
|
3. Au minimum : un commentaire en tête du helper "secondaire" qui pointe vers le canonique et liste explicitement les filtres à maintenir synchronisés
|
|
|
|
- Contexte technique : auth / RBAC — RL799_V2 27-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-guard-charge-objets-riches"></a>
|
|
## Guard d'autorisation qui charge des objets riches
|
|
|
|
### Risques
|
|
|
|
- Une guard d'autorisation s'exécute à CHAQUE requête sur une route protégée
|
|
- Si la guard a besoin de "trouver une candidate" (ex : "cette tenue est-elle dans les 'dernières rappelables' du grade pour une de mes délégations ?"), le repo helper utilisé doit avoir un select **minimal**, PAS le select complet utilisé par les services métier
|
|
- Pour un user avec N délégations actives, on charge N agrégats volumineux à chaque requête
|
|
|
|
### Symptômes
|
|
|
|
- Même fonction repo appelée par (1) un service qui a besoin de toutes les relations (rendu UI) et (2) une guard qui n'a besoin que de l'id
|
|
- La guard paie le coût du fetch riche inutilement
|
|
- Latence guard qui croît avec le nombre de relations chargées
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
Exposer **deux variantes** du repo helper :
|
|
|
|
- `findX(...)` — select riche, utilisé par les services métier
|
|
- `findXIdOnly(...)` — select `{ id: true }`, utilisé par les guards
|
|
|
|
```typescript
|
|
// Guard
|
|
export const requireXAccess = async (request, id, { roleSet }) => {
|
|
// utilise findXIdOnly (select minimal) — pas findX
|
|
const candidate = await repo.findXIdOnly({ ... });
|
|
if (!candidate || candidate.id !== id) return forbidden();
|
|
};
|
|
|
|
// Service métier
|
|
export const getXFullDetails = async (id) => {
|
|
return repo.findX({ ... }); // include riche
|
|
};
|
|
```
|
|
|
|
Coût : duplication de la clause `where` (acceptable, factorisable en constante). Bénéfice : la guard reste O(1) en payload même quand les relations grossissent.
|
|
|
|
- Contexte technique : auth / performance — RL799_V2 27-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-suppression-flag-auth-global"></a>
|
|
## Suppression d'un flag auth global (DB + DTO + tests) — cleanup atomique obligatoire
|
|
|
|
### Risques
|
|
|
|
- Un flag profondément câblé dans Prisma (ex : `mustChangePassword`, `isVerified`) ne peut pas être supprimé incrémentalement : chaque cleanup partiel produit un état non-compilable
|
|
- Les fixtures de tests qui posent `mustChangePassword: false` cassent à la compilation TS au moment du drop — bloque tout commit séparé
|
|
- Les helpers `helpers/db.ts` et les DTO partagés (`packages/shared`) sont prioritaires, sinon les imports cross-package échouent en cascade
|
|
|
|
### Symptômes
|
|
|
|
- `Property 'mustChangePassword' does not exist on type 'User'` après un drop partiel
|
|
- Tentative de découpage en sous-lots qui échoue au typecheck
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
Quand on prévoit de supprimer un flag auth profondément câblé :
|
|
|
|
1. **Le cleanup ne peut pas être incrémental** — soit on supprime tout dans un chantier, soit on garde le flag avec un nullable de transition
|
|
2. **Les fixtures de tests doivent être nettoyées dans le même PR** — grep systématique avant de démarrer (`grep -rn "mustChangePassword" apps/`) pour estimer l'ampleur
|
|
3. **Les helpers `helpers/db.ts`** sont prioritaires — un seul fichier touché casse tous les tests qui l'importent
|
|
4. **Les DTO partagés (`packages/shared`)** doivent être alignés en premier
|
|
5. Considérer un sous-lot dédié au cleanup si le flag est transverse — éviter de l'inclure dans un sous-lot fonctionnel
|
|
|
|
**Anti-pattern** : déprécier en douceur en gardant le flag avec un commentaire `// @deprecated` sans supprimer les usages. Le code mort s'accumule, les futurs devs hésitent à le nettoyer ("pourquoi c'est encore là ?"), la dépréciation ne se finit jamais.
|
|
|
|
- Contexte technique : auth / refactor schema — RL799_V2 28-04-2026 |