PD-241 — Plan d'implémentation¶
1. Références¶
- Spécification : PD-241-specification.md
- Tests : PD-241-tests.md
- Epic : PD-182 AUTH
- Projet cible : ProbatioVault-backend
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¶
- DTOs et Erreurs : Créer les classes d'erreur et DTO
- KeycloakAdminService : Ajouter invalidateSession() et revokeRefreshToken()
- AuthService : Ajouter logout()
- AuthController : Ajouter POST /auth/logout
- Tests unitaires : Couvrir tous les cas
- 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