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¶
- Strategy Pattern :
SessionStoreInterfaceavec implémentations Redis et Memory - Circuit Breaker : Bascule automatique Redis → Memory en < 1s
- Rate Limiting : Guard NestJS avec sliding window (Redis sorted sets)
- Timing-Safe Comparison :
crypto.timingSafeEqualpour 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.ts ← Ajout 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) :
- Stratégie de réconciliation : Last-Write-Wins avec timestamp
- Chaque session a un champ
lastModifiedAt(timestamp UTC ms) - Lors de la sync, comparer
session.lastModifiedAtmemory vs Redis -
La version la plus récente prévaut
-
Idempotence des opérations :
- Utiliser
SET NXavec TTL pour les nouvelles sessions - Pour les sessions existantes :
SETavec conditionIF lastModifiedAt > current -
Scripts Lua pour atomicité des comparaisons
-
Protection anti-flapping :
- Cooldown de 5s entre deux bascules (circuit breaker half-open)
-
Si 3 bascules en 60s → mode dégradé prolongé avec alerte
-
Traitement des sessions expirées :
- Ne pas synchroniser les sessions dont
expiresAt < Date.now() -
Log des sessions perdues pour audit
-
Événements :
FALLBACK_ONjournalisé à la bascule vers memoryFALLBACK_OFFjournalisé 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 :
- Stratégie : Fail-closed avec quota local conservateur
-
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).
-
Implémentation :
- Bascule sur rate limiting in-memory avec
Map<userId|IP, { count, windowStart }> - Seuils réduits : 1 req/user/15min (au lieu de 3), 2 req/IP/15min (au lieu de 5)
-
Justification : en mode dégradé mono-instance, les seuils réduits compensent l'absence de vue globale
-
Événement :
- Log
RATE_LIMIT_DEGRADED_MODEavec timestamp et scope affecté -
Compteur de requêtes traitées en mode dégradé pour monitoring
-
Retour mode nominal :
- Reset des compteurs in-memory (pas de sync, car les fenêtres sont courtes)
- 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-5 ← Correction 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