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>
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.mdpour 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.useren erreur (ou contournements involontaires) - Découvert tard (souvent uniquement en code review ou en prod)
Symptômes
request.uservautundefineddans 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 litrequest.user) - Ajouter au minimum 1 test d'intégration/e2e qui prouve que
request.userest 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.userIdsans null-check lève uneTypeError(500) sirequest.userest 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.usermocké 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
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
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,getThreadsou équivalent accessible sans vérification d'entitlements- Endpoint write protégé par
assertForumAccessmais 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
assertForumAccessou é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 metadataREQUIRE_ADMIN_ROLE_KEYposée par@RequireAdminRole()- Si le décorateur est absent,
requiresAdmin = false/undefined→ le guard retournetrueet 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.findFirstomet de filtrerexpiresAt→ 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 initialiserexpiresAtà +30j par défaut. Ajouter un helperseedExpiredSession()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
describee2e avecbuildApppartagé enbeforeAll(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 unitintermé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 unbuildAppisolé avecapp.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 —
undefinedest traité comme'', aucune erreur visible, l'UI se dégrade sans signal d'alerte
Symptômes
decodeJwtPayload(token).fieldretourneundefinedpour 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
decodeJwtPayloadcô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
JwtPayloadcôté frontend sans modification correspondante dansauthService.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.emaildans 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
undefinedpour le champ email dans l'annuaire -
Signal review :
email: user.emaildans 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+updatesé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
updateManyatomique avec conditionWHERE revokedAt IS NULL AND expiresAt > NOW()et vérifiercount === 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 :
findUniquesuivi deupdateséparés dans un flux de rotation de refresh token -
Contexte technique : auth / refresh token — RL799_V2 08-04-2026
Complément — rotation du refresh token IdP en BFF : cookie rafraîchi non réécrit → déconnexions erratiques
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 :
- Si la réécriture du cookie n'est pas généralisée à TOUS les handlers (via un wrapper qui attache
sessionCookieToApplysystématiquement), alors NE PAS activer la rotation du refresh token côté realm — et le documenter comme garde-fou de déploiement explicite. - Inversement, si on veut la rotation (recommandé en sécurité), généraliser la réécriture du cookie AVANT.
- 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
/meconsommesessionCookieToApply, ~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+verifyTokendirectement dans un service métier -
Si le helper ne couvre pas un besoin (ex: besoin de
userIden plus deemail), étendre le helper plutôt que contourner -
Signal review : import de
verifyTokendans un fichier service (horsauthHelpers.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: trueet un chemin d'AUTORISATION qui faitgetUserByEmailsans filtreisActive— 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ôleisActive. - 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
403trompeur (authz) au lieu d'un401(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 surstatus: 'published', closedAt: null, cancelledAt: nulldans un helper, justecancelledAt: nulldans 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
- Un seul helper canonique par notion d'activité (ex :
isDelegationActive,isSoireeOpenForRappel). Les autres l'appellent - 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
- 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étierfindXIdOnly(...)— 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: falsecassent à la compilation TS au moment du drop — bloque tout commit séparé - Les helpers
helpers/db.tset 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é :
- 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
- 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 - Les helpers
helpers/db.tssont prioritaires — un seul fichier touché casse tous les tests qui l'importent - Les DTO partagés (
packages/shared) doivent être alignés en premier - 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 danslogin()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érationdanspatterns/auth.md) appliqué au cas soft-delete.
Symptômes
login()lèveACCOUNT_DELETEDau lieu deINVALID_CREDENTIALSpour 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épondreINVALID_CREDENTIALSpour un compte soft-deleted — jamais un code spécifique. -
Nuance : un code
ACCOUNT_DELETEDreste acceptable dans un fluxexchange()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
expiresAtd'un jeton d'octroi (code de déblocage, lien/token d'invitation) comme SOURCE D'ACCÈS directe - Mais
expiresAtborne 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
expiresAtdu 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
UserPackdès le départ). -
Contexte technique : auth / activation vs possession — app-alexandrie 02-06-2026