Files
_Assistant_Lead_Tech/knowledge/backend/risques/auth.md
T
MaksTinyWorkshop f1b783407a 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>
2026-06-25 11:25:02 +02:00

26 KiB


title: Backend — Risques & vigilance : Auth domain: backend bucket: risques tags: [auth, guards, request-user, sessions, admin, enumeration, soft-delete] applies_to: [implementation, review, debug] severity: high validated_on: 2026-06-25 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

Angle distinct mais lié à la rotation : en archi BFF, si le cookie de session rafraîchi (sessionCookieToApply) n'est réécrit que par UN seul handler (typiquement /me), la rotation du refresh token côté IdP déconnecte les utilisateurs de façon erratique.

  • Les N autres call-sites d'auth déclenchent bien le refresh (access token rafraîchi en mémoire → requête autorisée → 200) mais JETTENT le nouveau cookie.
  • Tant que l'IdP n'a PAS la rotation activée, c'est inoffensif (l'ancien refresh token reste valide). MAIS si le realm a Revoke Refresh Token / rotation activée (durcissement prod COURANT chez Keycloak/Auth0/etc.), chaque refresh INVALIDE l'ancien refresh token côté IdP : le cookie non réécrit garde un refresh token révoqué → la requête suivante échoue → SESSION_EXPIRED → re-login forcé.
  • Piège : INVISIBLE en dev (rotation souvent off par défaut), il n'apparaît qu'au déploiement quand un ops active la rotation pour durcir.

Règles :

  1. Si la réécriture du cookie n'est pas généralisée à TOUS les handlers (via un wrapper qui attache sessionCookieToApply systématiquement), alors NE PAS activer la rotation du refresh token côté realm — et le documenter comme garde-fou de déploiement explicite.
  2. Inversement, si on veut la rotation (recommandé en sécurité), généraliser la réécriture du cookie AVANT.
  3. Ne jamais traiter « le refresh marche en dev » comme preuve que la rotation marchera en prod — tester avec la rotation activée.
  • Cas vécu : RL799 K1.5, seul /me consomme sessionCookieToApply, ~202 autres call-sites l'ignorent ; garde-fou « pas de rotation realm avant généralisation » renvoyé au Lot 6 déploiement — 15-06-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

Complément — cohérence des filtres d'autorisation entre TOUS les chemins ciblant la même population

Le drift ne touche pas que les codes d'erreur : il touche aussi les FILTRES appliqués sur la même population résolue à plusieurs endroits.

  • Quand une même population (ex. « les membres actifs d'un grade ») est résolue à plusieurs endroits — un chemin de NOTIFICATION qui filtre isActive: true et un chemin d'AUTORISATION qui fait getUserByEmail sans filtre isActive — la divergence crée une faille : un compte désactivé/démissionnaire avec un JWT encore valide (fenêtre ≤ TTL) n'est pas notifié MAIS peut encore agir.
  • Règle : tout contrôle d'autorisation basé sur un fetch user doit re-vérifier isActive à chaque requête (le JWT ne reflète pas une désactivation survenue après émission).
  • Audit : grep des getUserByEmail / findUser* dans les services, vérifier que chaque usage en contexte d'autorisation filtre/contrôle isActive.
  • Symptôme de l'incohérence : « la liste des destinataires d'un effet et la liste des autorisés à le déclencher ne coïncident pas ».
  • Cas vécu : isolation de réponse aux instructions RL799 — le fetch DB avait été ajouté EXPRÈS pour capter les changements d'état à chaque requête, mais ignorait isActive, annulant le bénéfice.

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

Information disclosure sur comptes soft-deleted dans login()

Risques

  • Retourner un code d'erreur distinct (ex : ACCOUNT_DELETED) pour les comptes supprimés dans login() permet l'énumération : un attaquant distingue « compte supprimé » de « identifiants invalides » par email.
  • Complément du pattern anti-énumération (cf. pattern-...anti-énumération dans patterns/auth.md) appliqué au cas soft-delete.

Symptômes

  • login() lève ACCOUNT_DELETED au lieu de INVALID_CREDENTIALS pour un compte soft-deleted
  • L'existence (et le statut) d'un compte fuite via la réponse d'authentification

Bonnes pratiques / mitigations

// ❌ DANGEREUX — révèle l'existence d'un compte supprimé
if (user.deletedAt !== null) {
  throw new UnauthorizedException({ error: { code: 'ACCOUNT_DELETED' } });
}

// ✅ CORRECT — même code que des identifiants invalides
if (user.deletedAt !== null) {
  throw new UnauthorizedException({
    error: { code: 'INVALID_CREDENTIALS', message: 'Email ou mot de passe invalide.' },
  });
}
  • Règle : dans login(), toujours répondre INVALID_CREDENTIALS pour un compte soft-deleted — jamais un code spécifique.

  • Nuance : un code ACCOUNT_DELETED reste acceptable dans un flux exchange() OAuth, où le provider a déjà confirmé l'identité (pas d'énumération possible côté attaquant).

  • Contexte technique : auth / soft-delete / anti-énumération — app-alexandrie 13-04-2026


Guard d'abonnement global vs droits acquis permanents

Risques

  • Un guard de gating « abonnement actif » (ex. RequireSubscriptionActive / RequireAccessLevel(FULL)) posé uniformément sur TOUTES les routes d'un domaine coupe l'accès à un contenu déjà payé en one-shot (« possession à vie ») dès que l'abonnement est résilié
  • Violation silencieuse d'un invariant métier : « je garde ce que j'ai payé même sans abo »

Symptômes

  • Couper l'abonnement rend inaccessible un contenu acheté de façon permanente
  • Aucun test ne couvre le cas « droit permanent + abo coupé » → régression non détectée

Bonnes pratiques / mitigations

  • Avant d'appliquer un guard « abonnement actif » uniformément, distinguer deux natures de droit :

    • droit RÉCURRENT (lié à l'abo : feed, communauté, contenu inclus)
    • droit ACQUIS/permanent (achat one-shot, possession « à vie »)
  • Règle : gater la LECTURE d'un bien acquis par la POSSESSION (helper canAccess…), pas par l'abonnement. Réserver le guard abo aux routes d'écriture/progression et aux contenus récurrents.

  • TOUJOURS écrire un test « bien possédé + abo coupé → lisible » : c'est l'angle mort classique qui laisse passer ce type de régression.

  • Contexte technique : auth / gating abonnement — app-alexandrie 02-06-2026


Confondre la validité du JETON d'octroi avec la durée de l'ACCÈS octroyé

Risques

  • Un helper d'accès lit le expiresAt d'un jeton d'octroi (code de déblocage, lien/token d'invitation) comme SOURCE D'ACCÈS directe
  • Mais expiresAt borne la fenêtre d'ACTIVATION du jeton (ex. 72 h), pas la durée de l'accès octroyé (censé être permanent) → l'accès expire en même temps que le jeton

Symptômes

  • L'accès « à vie » expire 72 h après l'émission du code
  • Bug non détecté par les tests (qui valident le helper tel qu'écrit, pas l'intention)

Bonnes pratiques / mitigations

  • Ne JAMAIS faire dépendre la vérification d'accès du expiresAt du jeton.

  • À l'activation, matérialiser l'accès dans son entité propre (ex. UserPack/possession) et vérifier l'accès via CETTE entité — pas via le jeton.

  • Règle : « le jeton expire, le droit qu'il a créé persiste. »

  • Test obligatoire : « jeton activé puis expiré → l'accès reste valide ».

  • Corollaire : un helper d'accès ne doit pas « anticiper » un mécanisme pas encore implémenté en lisant un état intermédiaire — il introduit un modèle d'accès parallèle qui diverge du modèle cible (la branche aurait dû passer par UserPack dès le départ).

  • Contexte technique : auth / activation vs possession — app-alexandrie 02-06-2026