Aller au contenu

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 :

{ password: string }

Response :

{ reauthToken: string, expiresAt: string }

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 :

{
  enabled: boolean;
  method: 'TOTP' | null;
  configuredAt: string | null;  // ISO 8601 UTC
}

TotpInitDto :

{
  secret: string;      // Base32
  qrCodeUri: string;   // otpauth://totp/...
  expiresAt: string;   // ISO 8601 UTC
}

TotpVerifyRequestDto :

{
  code: string;  // 6 digits
}

TotpVerifyResponseDto :

{
  enabled: boolean;
  recoveryCodes: string[];  // 15 codes
}

MfaDisableDto :

{
  enabled: false;
}

RecoveryRegenerateDto :

{
  recoveryCodes: string[];  // 15 nouveaux codes
}

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)

  1. keycloak-admin.config.ts : Configuration Keycloak Admin API
  2. keycloak-admin.service.ts : Client HTTP Keycloak

Phase 2 : Reauth (3 fichiers)

  1. reauth.service.ts : Génération reauth token
  2. reauth.controller.ts : Endpoint POST /auth/reauth
  3. reauth-token.guard.ts : Validation X-Reauth-Token

Phase 3 : Core MFA (5 fichiers)

  1. mfa-management.errors.ts : Classes d'erreurs ERR-238-*
  2. mfa-exception.filter.ts : Filter pour normaliser erreurs en réponses HTTP
  3. DTOs : Tous les DTOs (status, init, verify, disable, regenerate)
  4. mfa-management.service.ts : Logique métier
  5. mfa-management.controller.ts : 5 endpoints

Phase 4 : Guards & Module (3 fichiers)

  1. mfa-rate-limit.guard.ts : Rate limiting
  2. mfa.module.ts : Mise à jour imports/exports
  3. auth.module.ts : Mise à jour pour reauth

Phase 5 : Tests

  1. Tests unitaires services
  2. 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