Aller au contenu

PD-241 — Plan d'implémentation

1. Références

2. Découpage en composants

2.1 AuthController

Responsabilité : Exposer l'endpoint POST /auth/logout avec validation JWT.

Fichiers : - src/modules/auth/auth.controller.ts (modification)

Mécanismes : - Guard JwtAuthGuard pour validation du JWT (throw UnauthorizedException → 401) - Décorateur @HttpCode(200) pour réponse succès - Extraction du refresh_token optionnel depuis le body

2.2 AuthService

Responsabilité : Orchestrer le logout en déléguant à KeycloakAdminService.

Fichiers : - src/modules/auth/auth.service.ts (modification)

Mécanismes : - Méthode logout(userId: string, refreshToken?: string): Promise<void> - Appel à KeycloakAdminService.invalidateSession(userId) - Appel à KeycloakAdminService.revokeRefreshToken(userId, refreshToken) si présent - Gestion des erreurs Keycloak → codes ERR-241-*

2.3 KeycloakAdminService

Responsabilité : Communiquer avec Keycloak Admin API pour invalider session et refresh token.

Fichiers : - src/modules/auth/mfa/services/keycloak-admin.service.ts (modification)

Mécanismes : - Méthode invalidateSession(keycloakUserId: string): Promise<void> - DELETE /users/{id}/sessions (invalide toutes les sessions) - Méthode revokeRefreshToken(keycloakUserId: string, refreshToken: string): Promise<void> - POST /users/{id}/logout avec refresh token - Réutilisation du circuit breaker et retry existants

2.4 DTOs et Erreurs

Fichiers : - src/modules/auth/dto/logout.dto.ts (nouveau) - src/modules/auth/errors/logout.errors.ts (nouveau)

DTOs : - LogoutDto : { refresh_token?: string }

Erreurs : - LogoutUnauthenticatedError → ERR-241-UNAUTHENTICATED (401) - LogoutFailedError → ERR-241-LOGOUT-FAILED (502) - LogoutInternalError → ERR-241-INTERNAL (500)

2.5 Formatage des erreurs (HttpExceptionFilter)

Responsabilité : Transformer les exceptions typées en réponses JSON conformes à INV-241-04.

Fichiers : - src/common/filters/http-exception.filter.ts (existant, pas de modification)

Mécanisme de transformation :

Le HttpExceptionFilter global (déjà configuré dans main.ts) intercepte toutes les exceptions HTTP et les formate selon le contrat :

Exception levée                    → Réponse HTTP
─────────────────────────────────────────────────────────────────
UnauthorizedException              → 401 { error: "ERR-241-UNAUTHENTICATED", message: "..." }
  (levée par JwtAuthGuard)

LogoutFailedError                  → 502 { error: "ERR-241-LOGOUT-FAILED", message: "..." }
  (levée par AuthService.logout)

LogoutInternalError                → 500 { error: "ERR-241-INTERNAL", message: "..." }
  (levée en cas d'erreur inattendue)

Implémentation :

Les classes d'erreur LogoutFailedError et LogoutInternalError héritent de HttpException et définissent : - Le code HTTP (502 ou 500) - Le payload { error: "ERR-241-*", message: "..." }

Le filter extrait ce payload et le retourne tel quel. Pour UnauthorizedException (du guard), le filter applique un mapping explicite vers ERR-241-UNAUTHENTICATED.

Relation avec INV-241-04 : Ce mécanisme garantit que TOUTE erreur du flux logout est formatée en { error: "ERR-241-*", message: "..." } avant d'être renvoyée au client.

2.6 Tests

Fichiers : - src/modules/auth/auth.controller.spec.ts (modification) - src/modules/auth/auth.service.spec.ts (modification) - src/modules/auth/mfa/services/__tests__/keycloak-admin.service.spec.ts (modification) - test/auth/logout.e2e-spec.ts (nouveau)

3. Mapping Invariants → Mécanismes

Invariant Mécanisme Observable
INV-241-01 JwtAuthGuard sur endpoint 401 sans JWT
INV-241-02 KeycloakAdminService.invalidateSession() DELETE /users/{id}/sessions
INV-241-03 KeycloakAdminService.revokeRefreshToken() POST /users/{id}/logout
INV-241-04 HttpExceptionFilter + erreurs typées Format JSON
INV-241-05 try/catch + LogoutFailedError 502 + ERR-241-LOGOUT-FAILED
INV-241-06 N/A (hors périmètre) N/A

4. Mapping Critères → Tests

Critère Test(s) Observable
CA-241-01 TC-ERR-01 401 Unauthorized
CA-241-02 TC-NOM-01 Session Keycloak invalidée
CA-241-03 TC-NOM-02 Refresh token révoqué
CA-241-04 TC-ERR-02 502 + ERR-241-LOGOUT-FAILED
CA-241-05 TC-ERR-03 Format
CA-241-06 N/A Hors périmètre

5. Flux d'implémentation

F-241-01 — Déconnexion nominale

Client → POST /auth/logout
         Header: Authorization: Bearer <JWT>
         Body: { refresh_token?: "<token>" }

Controller:
  1. JwtAuthGuard valide le JWT
  2. Extrait userId du JWT (req.user.sub)
  3. Parse LogoutDto (refresh_token optionnel)
  4. Appelle AuthService.logout(userId, refreshToken?)

AuthService.logout():
  1. Appelle KeycloakAdminService.invalidateSession(userId)
  2. Si refreshToken présent:
     - Appelle KeycloakAdminService.revokeRefreshToken(userId, refreshToken)
  3. Retourne void (succès)

Controller:
  5. Retourne { success: true }

Flux d'erreur — JWT invalide

Client → POST /auth/logout (sans JWT ou JWT invalide)

JwtAuthGuard:
  1. Validation échoue
  2. Throw UnauthorizedException

HttpExceptionFilter:
  3. Transforme en { error: "ERR-241-UNAUTHENTICATED", message: "..." }
  4. Retourne 401 Unauthorized

Flux d'erreur — Échec Keycloak

KeycloakAdminService.invalidateSession():
  1. Circuit breaker vérifie état
  2. Exécute DELETE /users/{id}/sessions
  3. Si erreur 5xx ou réseau:
     - Retry avec backoff
     - Si tous retries échouent: throw LogoutFailedError

Controller:
  4. Catch LogoutFailedError
  5. Retourne 502 + { error: "ERR-241-LOGOUT-FAILED", message: "..." }

5bis. Diagrammes Mermaid

Graphe de dépendances des composants

graph TD
    Client([Client HTTP])
    AC[AuthController<br>§2.1]
    AS[AuthService<br>§2.2]
    KAS[KeycloakAdminService<br>§2.3]
    DTO[LogoutDto<br>§2.4]
    ERR[LogoutErrors<br>§2.4]
    HEF[HttpExceptionFilter<br>§2.5]
    KC[(Keycloak Admin API)]

    Client -->|POST /auth/logout| AC
    AC -->|JwtAuthGuard| AC
    AC -->|parse body| DTO
    AC -->|logout| AS
    AS -->|invalidateSession| KAS
    AS -->|revokeRefreshToken| KAS
    KAS -->|DELETE /users/id/sessions| KC
    KAS -->|POST /users/id/logout| KC
    AS -.->|throw| ERR
    ERR -.->|catch| HEF
    HEF -->|JSON {error, message}| Client

Diagramme de séquence — Flux nominal (F-241-01)

sequenceDiagram
    participant C as Client
    participant AC as AuthController §2.1
    participant JG as JwtAuthGuard
    participant AS as AuthService §2.2
    participant KAS as KeycloakAdminService §2.3
    participant KC as Keycloak Admin API

    C->>AC: POST /auth/logout<br>Authorization: Bearer JWT<br>Body: { refresh_token? }
    AC->>JG: validate JWT
    JG-->>AC: userId (req.user.sub)
    AC->>AS: logout(userId, refreshToken?)

    AS->>KAS: invalidateSession(userId)
    KAS->>KC: DELETE /users/{id}/sessions
    KC-->>KAS: 204 No Content
    KAS-->>AS: void

    opt refresh_token présent
        AS->>KAS: revokeRefreshToken(userId, refreshToken)
        KAS->>KC: POST /users/{id}/logout
        KC-->>KAS: 204 No Content
        KAS-->>AS: void
    end

    AS-->>AC: void
    AC-->>C: 200 { success: true }

Diagramme de séquence — Flux d'erreur Keycloak

sequenceDiagram
    participant C as Client
    participant AC as AuthController §2.1
    participant AS as AuthService §2.2
    participant KAS as KeycloakAdminService §2.3
    participant KC as Keycloak Admin API
    participant HEF as HttpExceptionFilter §2.5

    C->>AC: POST /auth/logout<br>Authorization: Bearer JWT
    AC->>AS: logout(userId)
    AS->>KAS: invalidateSession(userId)
    KAS->>KC: DELETE /users/{id}/sessions
    KC-->>KAS: 500 / timeout

    loop Retry avec backoff
        KAS->>KC: DELETE /users/{id}/sessions
        KC-->>KAS: 500 / timeout
    end

    KAS--xAS: throw LogoutFailedError (ERR-241-LOGOUT-FAILED)
    AS--xAC: propagate LogoutFailedError
    AC--xHEF: propagate LogoutFailedError
    HEF-->>C: 502 { error: "ERR-241-LOGOUT-FAILED", message: "..." }

6. Sécurité

6.1 Authentification

  • JwtAuthGuard OBLIGATOIRE sur l'endpoint
  • Pas de fallback anonyme
  • Rejet immédiat si JWT invalide ou expiré

6.2 Logging

  • Ne JAMAIS logger le refresh_token
  • Logger uniquement userId (tronqué) et résultat (success/failure)
  • Audit log pour conformité (event: 'user_logout')

6.3 Rate limiting

  • Pas de rate limiting sur logout (déjà authentifié)
  • Le JWT lui-même protège contre les abus

7. Hypothèses techniques

ID Hypothèse Impact si faux
H-241-01 Keycloak Admin API supporte DELETE /users/{id}/sessions Utiliser alternative POST /users/{id}/logout
H-241-02 Le refresh token est transmis dans le body JSON Adapter si header ou cookie
H-241-03 Le circuit breaker existant est compatible Tester avec cas d'erreur

8. Points de vigilance

  • Race condition : Deux logouts simultanés doivent tous deux réussir
  • Timeout : Utiliser le timeout existant de KeycloakAdminService (5s)
  • Atomicité INV-241-03 : Si le refresh token est présent et que revokeRefreshToken échoue, le logout DOIT échouer (ERR-241-LOGOUT-FAILED). L'invalidation de session seule n'est pas suffisante.

8.1 Hypothèses techniques additionnelles

ID Hypothèse Impact si faux
H-241-04 Le logout est idempotent (invalider une session déjà invalide = succès) Gérer les cas de retry

9. Ordre d'implémentation

  1. DTOs et Erreurs : Créer les classes d'erreur et DTO
  2. KeycloakAdminService : Ajouter invalidateSession() et revokeRefreshToken()
  3. AuthService : Ajouter logout()
  4. AuthController : Ajouter POST /auth/logout
  5. Tests unitaires : Couvrir tous les cas
  6. Tests E2E : Valider le flux complet

10. Estimation

Composant Effort
DTOs et Erreurs 0.5h
KeycloakAdminService 1h
AuthService 0.5h
AuthController 0.5h
Tests unitaires 2h
Tests E2E 1h
Total 5.5h

Plan généré par Claude Orchestrateur — 2026-02-07