Aller au contenu

PD-30 — Plan d'implémentation : Session Management Redis

1. Vue d'ensemble

Objectif

Implémenter le service de gestion de sessions Redis conforme à la spécification PD-30, incluant : - Création/expiration/renouvellement de sessions - Invalidation cross-instance avec SLA latence (p95 < 100ms, p99 < 250ms) - Rate limiting dual scope (user + IP) - Mode dégradé (fallback in-memory) - Journalisation probatoire

Complexité

MEDIUM — 8 tâches, ~15 fichiers, intégration Redis + patterns existants

Dépendances

Story Nature Statut
PD-28 Session revocation store (patterns) DONE
PD-31 Audit log append-only DONE
PD-238 Redis fallback patterns DONE
PD-240 Rate limiting guard DONE

2. Architecture technique

Structure des fichiers

src/
├── session/
│   ├── session.module.ts                    # Module NestJS
│   ├── session.service.ts                   # Service principal
│   ├── session.controller.ts                # Endpoints REST
│   ├── dto/
│   │   ├── create-session.dto.ts
│   │   ├── session-response.dto.ts
│   │   ├── device-list.dto.ts
│   │   └── revoke.dto.ts
│   ├── entities/
│   │   └── session.entity.ts                # Interface Session
│   ├── enums/
│   │   └── session-audit-event.enum.ts      # Enum partagé audit (ECT-04)
│   ├── guards/
│   │   └── session-rate-limit.guard.ts      # Rate limiting INV-30-15
│   ├── interceptors/
│   │   └── session-audit.interceptor.ts     # Journalisation INV-30-16
│   └── providers/
│       ├── redis-session.store.ts           # Store Redis principal
│       ├── memory-session.store.ts          # Fallback in-memory
│       └── session-store.interface.ts       # Interface commune
├── config/
│   └── session.config.ts                    # TTL par contexte
└── __tests__/
    └── session/
        ├── session.service.spec.ts
        ├── session.controller.spec.ts
        ├── redis-session.store.spec.ts
        ├── memory-session.store.spec.ts
        └── session-rate-limit.guard.spec.ts

Patterns architecturaux

  1. Strategy Pattern : SessionStoreInterface avec implémentations Redis et Memory
  2. Circuit Breaker : Bascule automatique Redis → Memory en < 1s
  3. Rate Limiting : Guard NestJS avec sliding window (Redis sorted sets)
  4. Timing-Safe Comparison : crypto.timingSafeEqual pour validation tokens

3. Mapping INV/CA → Tâches

Invariant/CA Tâche Fichier principal
INV-30-01, CA-30-01 TASK-1 session.service.ts
INV-30-02 TASK-1 session.service.ts (UUID v7)
INV-30-03, CA-30-02 TASK-1 session.entity.ts
INV-30-04, INV-30-07, CA-30-03, CA-30-06 TASK-2 session.config.ts
INV-30-05, CA-30-04 TASK-3 redis-session.store.ts
INV-30-06, CA-30-05 TASK-3 session.service.ts
INV-30-08, INV-30-12, CA-30-07 TASK-4 session.controller.ts
INV-30-09, INV-30-10, CA-30-08, CA-30-09 TASK-5 redis-session.store.ts
INV-30-11, CA-30-10 TASK-5 session.service.ts
INV-30-13, CA-30-11 TASK-5 session.service.ts
INV-30-14, CA-30-13 TASK-1 session.service.ts
INV-30-15, CA-30-12 TASK-6 session-rate-limit.guard.ts
INV-30-16, INV-30-17, CA-30-14 TASK-7 session-audit.interceptor.ts
INV-30-18, INV-30-19, CA-30-15, CA-30-16 TASK-3 memory-session.store.ts

4. Tâches détaillées

TASK-1 — Session Entity & Service Core

Agent : agent-developer Contract : CC-30-01 Fichiers : - src/session/entities/session.entity.ts - src/session/enums/session-audit-event.enum.tsAjout ECT-04 - src/session/session.service.ts - src/session/dto/*.dto.ts

Scope : - Interface Session avec 10 champs obligatoires (INV-30-03) - createSession() : génère UUID v7 (unicité INV-30-02), hash IP/UA, calcule expiresAt - validateSession() : comparaison timing-safe (INV-30-14) - DTOs pour création, réponse, révocation - Enum SessionAuditEvent (contrat partagé entre émetteurs et consommateur) :

export enum SessionAuditEvent {
  CREATE = 'CREATE',
  REFRESH = 'REFRESH',
  REVOKE_SESSION = 'REVOKE_SESSION',
  REVOKE_DEVICE = 'REVOKE_DEVICE',
  REVOKE_ALL = 'REVOKE_ALL',
  EXPIRE = 'EXPIRE',
  FALLBACK_ON = 'FALLBACK_ON',
  FALLBACK_OFF = 'FALLBACK_OFF',
}

Dépendances : Aucune


TASK-2 — Configuration TTL par contexte

Agent : agent-developer Contract : CC-30-02 Fichiers : - src/config/session.config.ts - src/session/session.module.ts

Scope : - Configuration injectable avec valeurs TTL contractuelles : - WEB: 3600s (1h) - MOBILE: 604800s (7j) - API: 86400s (24h) - SENSITIVE: 900s (15min) - Validation au démarrage (ECT-08) : - Bornes : min 60s, max 2592000s (30 jours) - Validation : class-validator avec décorateurs @IsInt(), @Min(60), @Max(2592000) - Comportement fail-fast : exception ConfigurationError au démarrage si config invalide, le module ne se charge pas

Dépendances : Aucune


TASK-3 — Session Stores (Redis + Fallback Memory)

Agent : agent-developer Contract : CC-30-03 Fichiers : - src/session/providers/session-store.interface.ts - src/session/providers/redis-session.store.ts - src/session/providers/memory-session.store.ts

Scope : - Interface commune SessionStoreInterface - Redis store avec TTL natif, pub/sub pour invalidation cross-instance - Memory store (ECT-07) : - Map<sessionId, { session: Session, expiresAt: number }> avec timestamp d'expiration - setInterval de purge batch toutes les 30s (pas de setTimeout par entrée) - Suppression des entrées où Date.now() > expiresAt - Circuit breaker : bascule Redis → Memory en < 1s (INV-30-18) - Rejet opérations globales en mode dégradé (INV-30-19)

Protocole de réconciliation fallback → Redis (ECT-03) :

Lors du retour en mode nominal (circuit breaker closed) :

  1. Stratégie de réconciliation : Last-Write-Wins avec timestamp
  2. Chaque session a un champ lastModifiedAt (timestamp UTC ms)
  3. Lors de la sync, comparer session.lastModifiedAt memory vs Redis
  4. La version la plus récente prévaut

  5. Idempotence des opérations :

  6. Utiliser SET NX avec TTL pour les nouvelles sessions
  7. Pour les sessions existantes : SET avec condition IF lastModifiedAt > current
  8. Scripts Lua pour atomicité des comparaisons

  9. Protection anti-flapping :

  10. Cooldown de 5s entre deux bascules (circuit breaker half-open)
  11. Si 3 bascules en 60s → mode dégradé prolongé avec alerte

  12. Traitement des sessions expirées :

  13. Ne pas synchroniser les sessions dont expiresAt < Date.now()
  14. Log des sessions perdues pour audit

  15. Événements :

  16. FALLBACK_ON journalisé à la bascule vers memory
  17. FALLBACK_OFF journalisé au retour vers Redis avec compteur de sessions synchronisées

Dépendances : TASK-1


TASK-4 — Session Controller & Device Listing

Agent : agent-developer Contract : CC-30-04 Fichiers : - src/session/session.controller.ts - src/session/dto/device-list.dto.ts

Scope : - GET /sessions : liste sessions utilisateur (INV-30-08) - GET /devices : liste appareils avec champs autorisés (INV-30-12) - Isolation stricte par userId (ECT-30-04 → 403) - Staleness <= 2s (CA-30-07) — ECT-06 : - Mode nominal : Lecture directe Redis sans cache applicatif → staleness = 0 - Mode dégradé : Lecture du memory store local → staleness potentiellement infinie (multi-instance) - Documentation : En mode dégradé, la garantie staleness <= 2s est suspendue (couvert par INV-30-19 qui restreint les opérations globales)

Dépendances : TASK-1, TASK-3


TASK-5 — Invalidation & Event Handlers

Agent : agent-developer Contract : CC-30-05 Fichiers : - src/session/session.service.ts (extension) - src/session/handlers/session-event.handler.ts

Scope : - revokeSession() : invalidation ciblée (INV-30-09) - revokeDevice() : invalidation par deviceId (INV-30-13) - revokeAll() : invalidation globale utilisateur (INV-30-10) - Event handlers : PASSWORD_CHANGED, DEVICE_REVOKED, INTRUSION_SUSPECTED, JUDICIAL_REQUEST - Comportement par défaut JUDICIAL_REQUEST : REVOKE_ALL + JUDICIAL_DEFAULT_SCOPE - Utilise l'enum SessionAuditEvent de TASK-1 pour les événements émis

Dépendances : TASK-3


TASK-6 — Rate Limiting Guard

Agent : agent-developer Contract : CC-30-06 Fichiers : - src/session/guards/session-rate-limit.guard.ts

Scope : - Sliding window avec Redis sorted sets - Dual scope : 3 req/user/15min + 5 req/IP/15min (INV-30-15) - Opérations sensibles : REVOKE_SESSION, REVOKE_DEVICE, REVOKE_ALL, LIST_USER_SESSIONS, LIST_USER_DEVICES - Réponse 429 avec retryAfterSeconds (ECT-30-02, ECT-30-03)

Mode dégradé (ECT-02) :

Comportement quand Redis est indisponible :

  1. Stratégie : Fail-closed avec quota local conservateur
  2. Justification sécuritaire : Les opérations protégées sont sensibles (REVOKE_ALL, etc.). Un bypass du rate limiting exposerait à des attaques par force brute. Un faux positif temporaire (rejet légitime) est préférable à un faux négatif (attaque non bloquée).

  3. Implémentation :

  4. Bascule sur rate limiting in-memory avec Map<userId|IP, { count, windowStart }>
  5. Seuils réduits : 1 req/user/15min (au lieu de 3), 2 req/IP/15min (au lieu de 5)
  6. Justification : en mode dégradé mono-instance, les seuils réduits compensent l'absence de vue globale

  7. Événement :

  8. Log RATE_LIMIT_DEGRADED_MODE avec timestamp et scope affecté
  9. Compteur de requêtes traitées en mode dégradé pour monitoring

  10. Retour mode nominal :

  11. Reset des compteurs in-memory (pas de sync, car les fenêtres sont courtes)
  12. Log RATE_LIMIT_NOMINAL_MODE

Dépendances : TASK-3


TASK-7 — Audit Interceptor

Agent : agent-developer Contract : CC-30-07 Fichiers : - src/session/interceptors/session-audit.interceptor.ts

Scope : - Journalisation append-only de tous les événements (INV-30-16) - Timestamp UTC milliseconde, acteur, motif - Événements : CREATE, REFRESH, REVOKE_SESSION, REVOKE_DEVICE, REVOKE_ALL, EXPIRE, FALLBACK_ON, FALLBACK_OFF - Aucun secret en clair (INV-30-17) - Intégration avec PD-31 (audit log) - Utilise l'enum SessionAuditEvent défini dans TASK-1 comme contrat d'interface

Dépendances : TASK-1, TASK-3, TASK-5Correction ECT-04

Note : TASK-7 dépend de TASK-3 (événements FALLBACK_ON/OFF) et TASK-5 (événements REVOKE_*) pour garantir la cohérence des types d'événements émis. L'enum partagé SessionAuditEvent (TASK-1) sert de contrat entre émetteurs et consommateur.


TASK-8 — Tests unitaires et d'intégration

Agent : agent-qa-unit-integration Contract : CC-30-08 Fichiers : - src/__tests__/session/*.spec.ts

Scope : - Tests unitaires pour chaque composant - Tests d'intégration avec Redis (testcontainers) - Couverture des 19 TC du cahier de tests - Tests de latence p95/p99 pour invalidation - Tests de rate limiting avec assertions temporelles

Matrice de traçabilité INV/CA → Tests (ECT-01) :

INV/CA TC-ID Fichier test describe/it block
INV-30-01, CA-30-01 TC-30-001 session.service.spec.ts describe('createSession') / it('should create session with valid userId')
INV-30-02 TC-30-002 session.service.spec.ts describe('createSession') / it('should generate UUID v7 sessionId')
INV-30-03, CA-30-02 TC-30-003 session.entity.spec.ts describe('Session') / it('should have 10 required fields')
INV-30-04, INV-30-07 TC-30-005 session.config.spec.ts describe('SessionConfig') / it('should have distinct TTL values')
INV-30-05, CA-30-04 TC-30-004 redis-session.store.spec.ts describe('set') / it('should expire after TTL')
INV-30-06, CA-30-05 TC-30-003 session.service.spec.ts describe('refreshSession') / it('should reject expired session')
INV-30-08, CA-30-07 TC-30-006 session.controller.spec.ts describe('GET /sessions') / it('should return only user sessions')
INV-30-09, CA-30-08 TC-30-007 session.service.spec.ts describe('revokeSession') / it('should invalidate with p95 < 100ms')
INV-30-10, CA-30-09 TC-30-008 session.service.spec.ts describe('revokeAll') / it('should invalidate all user sessions')
INV-30-11, CA-30-10 TC-30-009 session-event.handler.spec.ts describe('PASSWORD_CHANGED') / it('should trigger REVOKE_ALL')
INV-30-12 TC-30-006 session.controller.spec.ts describe('GET /devices') / it('should return allowed fields only')
INV-30-13, CA-30-11 TC-30-010 session.service.spec.ts describe('revokeDevice') / it('should invalidate device sessions')
INV-30-14, CA-30-13 TC-30-011 session.service.spec.ts describe('validateSession') / it('should use timing-safe comparison')
INV-30-15, CA-30-12 TC-30-012, TC-30-013 session-rate-limit.guard.spec.ts describe('canActivate') / it('should block 4th user request')
INV-30-16, CA-30-14 TC-30-014 session-audit.interceptor.spec.ts describe('intercept') / it('should log all event types')
INV-30-17 TC-30-015 session-audit.interceptor.spec.ts describe('intercept') / it('should not log secrets')
INV-30-18, CA-30-15 TC-30-016, TC-30-017 memory-session.store.spec.ts describe('fallback') / it('should switch in < 1s')
INV-30-19, CA-30-16 TC-30-018 memory-session.store.spec.ts describe('degraded mode') / it('should reject global operations')
TC-30-019 session.service.spec.ts describe('JUDICIAL_REQUEST') / it('should default to REVOKE_ALL')

Protocole de mesure SLA invalidation (ECT-05) :

Paramètre Valeur Justification
Volume minimum 1000 invalidations Stabilité statistique des percentiles
Environnement CI Testcontainers Redis single-node Reproductibilité
Seuils CI (tolérance x2) p95 < 200ms, p99 < 500ms Variabilité CI/CD
Seuils production p95 < 100ms, p99 < 250ms Contractuels
Test dedicated Séparé des tests unitaires, job CI distinct Pas de flakiness

Implémentation :

describe('invalidation SLA', () => {
  it('should meet p95 < 200ms and p99 < 500ms in CI', async () => {
    const latencies: number[] = [];
    for (let i = 0; i < 1000; i++) {
      const start = performance.now();
      await sessionService.revokeSession(sessionIds[i]);
      latencies.push(performance.now() - start);
    }
    const sorted = latencies.sort((a, b) => a - b);
    const p95 = sorted[Math.floor(0.95 * 1000)];
    const p99 = sorted[Math.floor(0.99 * 1000)];
    expect(p95).toBeLessThan(200); // CI tolerance
    expect(p99).toBeLessThan(500); // CI tolerance
  });
});

Dépendances : TASK-1 à TASK-7

5. Contraintes techniques

Dépendances inter-PD

Story Statut Nature
PD-28 DONE Patterns revocation store
PD-31 DONE Interface audit log
PD-238 DONE Redis fallback patterns
PD-240 DONE Rate limiting patterns

Framework de test

  • Runner : Jest
  • Tests d'intégration : Testcontainers (Redis réel)
  • Mocks : jest-mock-extended pour stores
  • Variables CI : REDIS_URL, CI=true

Compatibilité ESM/CJS

  • Pas de dépendances ESM-only identifiées
  • Jest compatible avec la config existante

6. Séquencement

graph TD
    T1[TASK-1: Entity & Service] --> T3[TASK-3: Stores]
    T2[TASK-2: Config TTL] --> T3
    T3 --> T4[TASK-4: Controller]
    T3 --> T5[TASK-5: Invalidation]
    T3 --> T6[TASK-6: Rate Limiting]
    T1 --> T7[TASK-7: Audit]
    T3 --> T7
    T5 --> T7
    T4 --> T8[TASK-8: Tests]
    T5 --> T8
    T6 --> T8
    T7 --> T8

Ordre d'exécution : T1, T2 → T3 → T4, T5, T6 (parallélisables) → T7 → T8

Note : T7 dépend maintenant de T3 et T5 pour les événements (ECT-04).

7. Risques et mitigations

Risque Impact Mitigation
Latence invalidation > SLA Élevé Pub/sub Redis + tests de charge avec protocole défini (1000 ops, p95/p99)
Race condition création session Moyen UUID v7 + transaction Redis
Perte données fallback → Redis Moyen Protocole de réconciliation last-write-wins avec idempotence
Memory leak fallback Moyen TTL strict + cleanup batch toutes les 30s (setInterval)
Rate limiting bypass en fallback Élevé Fail-closed avec quotas conservateurs (1 req/user/15min)
Flapping circuit breaker Moyen Cooldown 5s, alerte si 3 bascules/60s

8. Critères de succès

  • 19/19 INV couverts par le code
  • 16/16 CA vérifiables par tests
  • Coverage >= 80%
  • Latence invalidation p95 < 100ms, p99 < 250ms (mesuré avec protocole défini)
  • Tests passants en local et CI
  • Linter OK (0 errors, 0 warnings)
  • Matrice traçabilité INV/CA → TC-ID complète et vérifiable