PD-238 — Plan d'implémentation¶
1. Vue d'ensemble¶
Objectif : Implémenter 6 endpoints de gestion MFA utilisateur dans le module auth existant.
Approche : Étendre le module MFA existant (PD-27) avec un nouveau controller et service dédié à la gestion MFA, en déléguant toutes les opérations à Keycloak Admin API.
2. Architecture cible¶
src/modules/auth/mfa/
├── mfa.module.ts # [MODIFIER] Ajouter imports
├── controllers/
│ └── mfa-management.controller.ts # [NOUVEAU] 5 endpoints /user/mfa/*
├── services/
│ ├── mfa-management.service.ts # [NOUVEAU] Logique métier
│ └── keycloak-admin.service.ts # [NOUVEAU] Client Keycloak Admin API
├── guards/
│ └── reauth-token.guard.ts # [NOUVEAU] Validation X-Reauth-Token
├── dto/
│ ├── mfa-status.dto.ts # [NOUVEAU] Response status
│ ├── totp-init.dto.ts # [NOUVEAU] Response init
│ ├── totp-verify.dto.ts # [NOUVEAU] Request/Response verify
│ ├── mfa-disable.dto.ts # [NOUVEAU] Response disable
│ └── recovery-regenerate.dto.ts # [NOUVEAU] Response regenerate
├── errors/
│ └── mfa-management.errors.ts # [NOUVEAU] ERR-238-* errors
├── filters/
│ └── mfa-exception.filter.ts # [NOUVEAU] Normalisation erreurs HTTP
└── config/
└── keycloak-admin.config.ts # [NOUVEAU] Config Admin API
src/modules/auth/
├── auth.module.ts # [MODIFIER] Ajouter ReauthModule
├── controllers/
│ └── reauth.controller.ts # [NOUVEAU] POST /auth/reauth
├── services/
│ └── reauth.service.ts # [NOUVEAU] Génération reauth token
└── guards/
└── mfa-rate-limit.guard.ts # [NOUVEAU] Rate limit MFA
3. Composants à implémenter¶
3.1 Keycloak Admin Client¶
Fichier : src/modules/auth/mfa/services/keycloak-admin.service.ts
Responsabilités : - Authentification service account vers Keycloak Admin API - Récupération état MFA utilisateur - Initialisation TOTP (obtenir secret + QR code) - Suppression credential MFA - Régénération codes de récupération
Méthodes :
getMfaStatus(keycloakUserId: string): Promise<MfaStatusResult>
initTotp(keycloakUserId: string): Promise<TotpInitResult>
verifyAndActivateTotp(keycloakUserId: string, code: string): Promise<TotpActivationResult>
disableMfa(keycloakUserId: string): Promise<void>
regenerateRecoveryCodes(keycloakUserId: string): Promise<string[]>
Sécurité : - Credentials service account via Vault - Token rotation automatique - Aucun secret MFA en logs (INV-238-09) - Aucun stockage local secrets (INV-238-10)
3.2 MFA Management Service¶
Fichier : src/modules/auth/mfa/services/mfa-management.service.ts
Responsabilités : - Orchestration des opérations MFA - Mapping userId local → keycloakUserId (via claim sub du JWT OIDC) - Gestion du sessionId TOTP (cache Redis entre init et verify) - Validation business rules - Émission événements sécurité
Mapping identité : - Le keycloakUserId est extrait du claim sub du token JWT OIDC - Ce claim est garanti unique par Keycloak et correspond à l'UUID utilisateur Keycloak - Aucune table de correspondance nécessaire (source d'autorité = claim OIDC)
Gestion sessionId TOTP : - initTotp retourne un sessionId (ID session Keycloak pour l'init) - Ce sessionId est stocké en cache Redis (clé: mfa:totp:session:{userId}, TTL: 5min) - verifyTotp récupère le sessionId du cache et le passe à Keycloak - Après activation réussie, le cache est nettoyé
Méthodes :
getStatus(userId: string): Promise<MfaStatusDto>
initTotp(userId: string): Promise<TotpInitDto>
verifyTotp(userId: string, code: string): Promise<TotpVerifyResponseDto>
disable(userId: string): Promise<MfaDisableDto>
regenerateRecoveryCodes(userId: string): Promise<RecoveryRegenerateDto>
3.3 MFA Management Controller¶
Fichier : src/modules/auth/mfa/controllers/mfa-management.controller.ts
Endpoints :
| Route | Méthode | Guards | Description |
|---|---|---|---|
/user/mfa/status | GET | OidcJwtAuthGuard, MfaRateLimitGuard | État MFA |
/user/mfa/totp/init | POST | OidcJwtAuthGuard, MfaRateLimitGuard | Init TOTP |
/user/mfa/totp/verify | POST | OidcJwtAuthGuard, MfaRateLimitGuard | Verify + activate |
/user/mfa/disable | POST | OidcJwtAuthGuard, ReauthTokenGuard, MfaRateLimitGuard | Disable |
/user/mfa/recovery/regenerate | POST | OidcJwtAuthGuard, ReauthTokenGuard, MfaRateLimitGuard | Regen codes |
3.4 Reauth Controller & Service¶
Fichier : src/modules/auth/controllers/reauth.controller.ts
Endpoint : POST /auth/reauth
Guards : OidcJwtAuthGuard, MfaRateLimitGuard
Payload :
Response :
Logique : 1. Extraire userId du JWT 2. Vérifier mot de passe via SRP (réutiliser SrpService) 3. Générer JWT signé : { sub, purpose: "reauth", exp: now + 5min } 4. Retourner token + expiresAt
3.5 Reauth Token Guard¶
Fichier : src/modules/auth/mfa/guards/reauth-token.guard.ts
Responsabilités : - Extraire header X-Reauth-Token - Valider signature JWT (même clé que tokens principaux) - Vérifier purpose === "reauth" - Vérifier sub correspond au JWT principal - Vérifier expiration (5 minutes)
Erreur : ERR-238-UNAUTHORIZED-REAUTH (401)
3.6 MFA Rate Limit Guard¶
Fichier : src/modules/auth/guards/mfa-rate-limit.guard.ts
Limites : - 10 requêtes/minute par userId sur endpoints MFA - Clé Redis : rate_limit:mfa:{userId}
Erreur : ERR-238-RATE-LIMIT (429)
3.7 DTOs¶
MfaStatusDto :
TotpInitDto :
{
secret: string; // Base32
qrCodeUri: string; // otpauth://totp/...
expiresAt: string; // ISO 8601 UTC
}
TotpVerifyRequestDto :
TotpVerifyResponseDto :
MfaDisableDto :
RecoveryRegenerateDto :
3.8 Erreurs¶
Fichier : src/modules/auth/mfa/errors/mfa-management.errors.ts
| Code | HTTP | Condition |
|---|---|---|
| ERR-238-UNAUTHENTICATED | 401 | JWT absent/invalide |
| ERR-238-UNAUTHORIZED-REAUTH | 401 | Reauth token absent/invalide/expiré |
| ERR-238-FORBIDDEN-CROSS-ACCESS | 403 | Accès MFA autre utilisateur |
| ERR-238-TOTP-INIT-FAILED | 500 | Échec init Keycloak |
| ERR-238-TOTP-INVALID | 400 | Code TOTP invalide |
| ERR-238-MFA-DISABLE-FAILED | 500 | Échec disable Keycloak |
| ERR-238-RECOVERY-REGEN-FAILED | 500 | Échec regen Keycloak |
| ERR-238-RATE-LIMIT | 429 | Seuil dépassé |
| ERR-238-REAUTH-FAILED | 401 | Mot de passe invalide |
| ERR-238-INTERNAL | 500 | Erreur interne |
4. Séquence d'implémentation¶
Phase 1 : Infrastructure (2 fichiers)¶
- keycloak-admin.config.ts : Configuration Keycloak Admin API
- keycloak-admin.service.ts : Client HTTP Keycloak
Phase 2 : Reauth (3 fichiers)¶
- reauth.service.ts : Génération reauth token
- reauth.controller.ts : Endpoint POST /auth/reauth
- reauth-token.guard.ts : Validation X-Reauth-Token
Phase 3 : Core MFA (5 fichiers)¶
- mfa-management.errors.ts : Classes d'erreurs ERR-238-*
- mfa-exception.filter.ts : Filter pour normaliser erreurs en réponses HTTP
- DTOs : Tous les DTOs (status, init, verify, disable, regenerate)
- mfa-management.service.ts : Logique métier
- mfa-management.controller.ts : 5 endpoints
Phase 4 : Guards & Module (3 fichiers)¶
- mfa-rate-limit.guard.ts : Rate limiting
- mfa.module.ts : Mise à jour imports/exports
- auth.module.ts : Mise à jour pour reauth
Phase 5 : Tests¶
- Tests unitaires services
- Tests e2e endpoints
5. Dépendances externes¶
| Dépendance | Usage | Existante |
|---|---|---|
| @nestjs/jwt | Signature reauth token | Oui |
| @nestjs/axios | Client HTTP Keycloak | Oui |
| ioredis | Rate limiting | Oui |
| class-validator | Validation DTOs | Oui |
| class-transformer | Transformation DTOs | Oui |
Aucune nouvelle dépendance npm requise.
6. Configuration requise¶
Variables d'environnement (à ajouter) :
KEYCLOAK_ADMIN_URL=https://auth.probatiovault.com/admin/realms/{realm}
KEYCLOAK_SERVICE_ACCOUNT_CLIENT_ID=pv-backend-admin
KEYCLOAK_SERVICE_ACCOUNT_CLIENT_SECRET=<vault>
MFA_RATE_LIMIT_MAX=10
MFA_RATE_LIMIT_WINDOW_MS=60000
REAUTH_TOKEN_EXPIRY_SECONDS=300
Permissions Keycloak Admin API : Le service account pv-backend-admin doit avoir les permissions suivantes : - Scope : openid - Realm Role : manage-users (permet CRUD sur credentials utilisateurs) - Fine-grained permissions (alternative) : - view-users : GET /users/{id}/credentials - manage-users : POST/DELETE /users/{id}/credentials/*
Configuration Keycloak : 1. Créer client pv-backend-admin (confidential, service account enabled) 2. Assigner role manage-users au service account 3. Stocker client_secret dans Vault à kv/app/keycloak-admin
7. Mapping Keycloak Admin API¶
| Opération PD-238 | Keycloak Admin API |
|---|---|
| getMfaStatus | GET /users/{id}/credentials |
| initTotp | POST /users/{id}/credentials/otp/init |
| verifyTotp | POST /users/{id}/credentials/otp/verify |
| disableMfa | DELETE /users/{id}/credentials/{credId} |
| regenerateRecoveryCodes | POST /users/{id}/credentials/{credId}/recovery-codes |
8. Diagrammes Mermaid¶
8.1 Graphe de dépendances des composants¶
graph TD
subgraph "Module Auth"
RC[reauth.controller.ts]
RS[reauth.service.ts]
AM[auth.module.ts]
MRLG[mfa-rate-limit.guard.ts]
end
subgraph "Module MFA"
MMC[mfa-management.controller.ts]
MMS[mfa-management.service.ts]
KAS[keycloak-admin.service.ts]
RTG[reauth-token.guard.ts]
KAC[keycloak-admin.config.ts]
MME[mfa-management.errors.ts]
MEF[mfa-exception.filter.ts]
DTOs[DTOs]
MM[mfa.module.ts]
end
subgraph "Externe"
KC[(Keycloak Admin API)]
REDIS[(Redis)]
VAULT[(HashiCorp Vault)]
end
subgraph "Existant"
OIDC[OidcJwtAuthGuard]
SRP[SrpService]
JWT[@nestjs/jwt]
end
MMC --> MMS
MMC --> RTG
MMC --> MRLG
MMC --> OIDC
MMC --> DTOs
MMC --> MEF
MMS --> KAS
MMS --> REDIS
MMS --> MME
KAS --> KAC
KAS --> KC
KAS --> VAULT
RC --> RS
RC --> OIDC
RC --> MRLG
RS --> SRP
RS --> JWT
RTG --> JWT
MRLG --> REDIS
MM --> MMC
MM --> MMS
MM --> KAS
MM --> RTG
AM --> MM
AM --> RC
AM --> RS 8.2 Séquence — Activation TOTP (init + verify)¶
sequenceDiagram
actor U as Utilisateur
participant C as MfaManagementController
participant G as Guards (JWT + RateLimit)
participant S as MfaManagementService
participant R as Redis
participant K as KeycloakAdminService
participant KC as Keycloak Admin API
Note over U,KC: Phase 1 — Initialisation TOTP
U->>C: POST /user/mfa/totp/init (JWT)
C->>G: OidcJwtAuthGuard + MfaRateLimitGuard
G-->>C: OK (userId extrait du claim sub)
C->>S: initTotp(userId)
S->>K: initTotp(keycloakUserId)
K->>KC: POST /users/{id}/credentials/otp/init
KC-->>K: {secret, qrCodeUri, sessionId}
K-->>S: TotpInitResult
S->>R: SET mfa:totp:session:{userId} = sessionId (TTL 5min)
S-->>C: TotpInitDto {secret, qrCodeUri, expiresAt}
C-->>U: 200 {secret, qrCodeUri, expiresAt}
Note over U,KC: Phase 2 — Vérification + Activation
U->>C: POST /user/mfa/totp/verify {code: "123456"} (JWT)
C->>G: OidcJwtAuthGuard + MfaRateLimitGuard
G-->>C: OK
C->>S: verifyTotp(userId, code)
S->>R: GET mfa:totp:session:{userId}
R-->>S: sessionId
S->>K: verifyAndActivateTotp(keycloakUserId, code)
K->>KC: POST /users/{id}/credentials/otp/verify
KC-->>K: {enabled: true, recoveryCodes: [...]}
K-->>S: TotpActivationResult
S->>R: DEL mfa:totp:session:{userId}
S-->>C: TotpVerifyResponseDto {enabled, recoveryCodes}
C-->>U: 200 {enabled: true, recoveryCodes: [15 codes]} 8.3 Séquence — Désactivation MFA (reauth + disable)¶
sequenceDiagram
actor U as Utilisateur
participant RAC as ReauthController
participant RAS as ReauthService
participant SRP as SrpService
participant JWT as JwtService
participant MMC as MfaManagementController
participant RTG as ReauthTokenGuard
participant MMS as MfaManagementService
participant KAS as KeycloakAdminService
participant KC as Keycloak Admin API
Note over U,KC: Phase 1 — Re-authentification
U->>RAC: POST /auth/reauth {password} (JWT)
RAC->>RAS: reauth(userId, password)
RAS->>SRP: verifyPassword(userId, password)
SRP-->>RAS: OK
RAS->>JWT: sign({sub, purpose: "reauth", exp: +5min})
JWT-->>RAS: reauthToken
RAS-->>RAC: {reauthToken, expiresAt}
RAC-->>U: 200 {reauthToken, expiresAt}
Note over U,KC: Phase 2 — Désactivation MFA
U->>MMC: POST /user/mfa/disable (JWT + X-Reauth-Token)
MMC->>RTG: Valider X-Reauth-Token
RTG->>JWT: verify(token)
JWT-->>RTG: {sub, purpose: "reauth"}
RTG-->>MMC: OK (purpose=reauth, sub match)
MMC->>MMS: disable(userId)
MMS->>KAS: disableMfa(keycloakUserId)
KAS->>KC: DELETE /users/{id}/credentials/{credId}
KC-->>KAS: 204
KAS-->>MMS: void
MMS-->>MMC: MfaDisableDto {enabled: false}
MMC-->>U: 200 {enabled: false} 9. Risques et mitigations¶
| Risque | Impact | Mitigation |
|---|---|---|
| Keycloak Admin API indisponible | Endpoints MFA inopérants | Circuit breaker + retry |
| Mapping userId → keycloakId incorrect | Accès croisé | Vérification stricte via claims |
| Secrets loggés accidentellement | Fuite sécurité | Interceptor exclusion + tests INV-02 |
| Token reauth replay | Attaque | exp court (5min) + audit événements |
10. Critères de complétion¶
- Tous les endpoints répondent selon spec
- Guards JWT + Reauth fonctionnels
- Rate limiting appliqué
- Erreurs ERR-238-* conformes
- Aucun secret dans logs (TC-INV-02)
- Tests e2e passent (14 tests)
- Coverage > 80%
Références¶
- Spécification : PD-238-specification.md
- Tests : PD-238-tests.md
- Module MFA existant : src/modules/auth/mfa/
- PD-27 : Validation MFA via claims OIDC