Files
_Assistant_Lead_Tech/knowledge/backend/risques/auth.md
MaksTinyWorkshop b3417ad77b capitalisation: intégration ~60 entrées RL799_V2 (triage 2026-05-02)
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>
2026-05-02 22:12:44 +02:00

19 KiB

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.


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

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é

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

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

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

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


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

// ✅ 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


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 :

// ✅ 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


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 :

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


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


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


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


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


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


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


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

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
// 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

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