Aller au contenu

PD-239 — Plan d'implémentation

1. Informations générales

Champ Valeur
User Story PD-239
Titre Changement de mot de passe utilisateur
Projet ProbatioVault-backend
Date 2026-02-06

2. Documents de référence

  • Spécification : PD-239-specification.md
  • Tests : PD-239-tests.md
  • Dépendances : PD-238 (reauth), PD-26 (Keycloak OIDC), PD-28 (session revocation)

3. Découpage en composants

3.1 Module password-change

Composant Fichier Responsabilité
Controller password-change.controller.ts Endpoint POST /user/password/change
Service password-change.service.ts Orchestration changement + invalidation
DTO password-change.dto.ts Validation entrée/sortie
Errors password-change.errors.ts Classes erreur ERR-239-*
Filter password-change-exception.filter.ts Normalisation JSON erreurs
Guard reauth-token.guard.ts Validation reauth token (header)

3.2 Extension Keycloak Admin Service

Composant Fichier Responsabilité
Method keycloak-admin.service.ts Nouvelle méthode changeUserPassword()

3.3 Réutilisation existante

Composant Source Usage
OidcJwtAuthGuard guards/oidc-jwt-auth.guard.ts Validation JWT principal
SessionRevocationStore session/stores/session-revocation.store.ts Invalidation sessions
UserRepository repositories/user.repository.ts Récupération user avec srpSalt/passwordHash
AuditService audit/audit.service.ts Événements audit
CurrentUser decorators/current-user.decorator.ts Injection JWT payload

Note : La vérification de l'ancien mot de passe (INV-239-03) est effectuée directement dans PasswordChangeService.verifyOldPassword() via SRP verifier et crypto.timingSafeEqual(). ReauthService (PD-238) n'est PAS utilisé pour cette vérification — il sert uniquement au guard ReauthTokenGuard pour valider le header X-Reauth-Token.

4. Architecture des fichiers

src/modules/auth/
├── password-change/
│   ├── password-change.controller.ts
│   ├── password-change.service.ts
│   ├── password-change.dto.ts
│   ├── password-change.errors.ts
│   ├── password-change-exception.filter.ts
│   ├── guards/
│   │   └── reauth-token.guard.ts
│   └── password-change.module.ts
├── mfa/services/
│   └── keycloak-admin.service.ts  (extension)
└── session/stores/
    └── session-revocation.store.ts  (réutilisation)

5. Flux d'exécution

F-239-01 — Changement de mot de passe nominal

┌─────────────────────────────────────────────────────────────────────┐
│ 1. Client → POST /user/password/change                              │
│    Headers: Authorization: Bearer <JWT>                             │
│             X-Reauth-Token: <reauth_token>                         │
│    Body: { "oldPassword": "...", "newPassword": "..." }            │
├─────────────────────────────────────────────────────────────────────┤
│ 2. OidcJwtAuthGuard → Valide JWT (INV-239-01)                       │
│    - Si invalide → ERR-239-UNAUTHENTICATED (401)                   │
├─────────────────────────────────────────────────────────────────────┤
│ 3. ReauthTokenGuard → Valide X-Reauth-Token (INV-239-02)           │
│    - Vérifie signature, exp, purpose="reauth", sub=jwt.sub         │
│    - Si invalide → ERR-239-UNAUTHORIZED-REAUTH (401)               │
├─────────────────────────────────────────────────────────────────────┤
│ 4. PasswordChangeService.verifyOldPassword() (INV-239-03)          │
│    - Récupère user (status=ACTIVE)                                  │
│    - Vérifie oldPassword via SRP verifier (timingSafeEqual)        │
│    - Si invalide → ERR-239-PWD-INVALID (400)                       │
├─────────────────────────────────────────────────────────────────────┤
│ 5. KeycloakAdminService.changeUserPassword() (INV-239-04, INV-239-08)│
│    - Appelle PUT /admin/realms/{realm}/users/{id}/reset-password   │
│    - Keycloak valide politique de complexité                        │
│    - Si politique non respectée → ERR-239-PWD-POLICY (400)         │
│    - Émet log event=keycloak_password_change (INV-239-10)          │
├─────────────────────────────────────────────────────────────────────┤
│ 6. SessionRevocationStore.revokeAllUserSessions() (INV-239-05)     │
│    - Révoque toutes les sessions (reason=PASSWORD_CHANGE)          │
│    - Délai ≤ 30 secondes                                           │
│    - Si échec → ERR-239-SESSION-INVALIDATION-FAILED (500)          │
│    - Note: changement déjà effectué, non annulé                    │
├─────────────────────────────────────────────────────────────────────┤
│ 7. AuditService.logEvent('PASSWORD_CHANGE_SUCCESS')                 │
├─────────────────────────────────────────────────────────────────────┤
│ 8. Réponse → 200 OK { "success": true }                            │
└─────────────────────────────────────────────────────────────────────┘

5bis. Diagrammes Mermaid

D-239-01 — Graphe de dépendances des composants

graph TD
    subgraph "Module password-change"
        CTRL["password-change.controller.ts"]
        SVC["password-change.service.ts"]
        DTO["password-change.dto.ts"]
        ERR["password-change.errors.ts"]
        FILT["password-change-exception.filter.ts"]
        GUARD_REAUTH["reauth-token.guard.ts"]
    end

    subgraph "Modules réutilisés"
        GUARD_JWT["OidcJwtAuthGuard<br/>(guards/oidc-jwt-auth.guard.ts)"]
        KC["KeycloakAdminService<br/>(mfa/services/keycloak-admin.service.ts)"]
        SESS["SessionRevocationStore<br/>(session/stores/session-revocation.store.ts)"]
        AUDIT["AuditService<br/>(audit/audit.service.ts)"]
        USER_REPO["UserRepository<br/>(repositories/user.repository.ts)"]
        CURRENT_USER["@CurrentUser<br/>(decorators/current-user.decorator.ts)"]
    end

    subgraph "Externes"
        KEYCLOAK["Keycloak Admin API"]
        REDIS["Redis (sessions)"]
    end

    CTRL -->|valide payload| DTO
    CTRL -->|appelle| SVC
    CTRL -->|protégé par| GUARD_JWT
    CTRL -->|protégé par| GUARD_REAUTH
    CTRL -->|injecte user| CURRENT_USER
    CTRL -->|filtre erreurs| FILT
    FILT -->|normalise| ERR
    SVC -->|vérifie ancien mdp| USER_REPO
    SVC -->|change mdp| KC
    SVC -->|révoque sessions| SESS
    SVC -->|émet événements| AUDIT
    SVC -->|lance erreurs| ERR
    KC -->|PUT reset-password| KEYCLOAK
    SESS -->|revokeAll| REDIS

D-239-02 — Diagramme de séquence du flux nominal

sequenceDiagram
    participant C as Client
    participant CTRL as PasswordChangeController
    participant JWT as OidcJwtAuthGuard
    participant RA as ReauthTokenGuard
    participant SVC as PasswordChangeService
    participant REPO as UserRepository
    participant KC as KeycloakAdminService
    participant KEYCLOAK as Keycloak API
    participant SESS as SessionRevocationStore
    participant REDIS as Redis
    participant AUDIT as AuditService

    C->>CTRL: POST /user/password/change<br/>{oldPassword, newPassword}<br/>+ JWT + X-Reauth-Token

    CTRL->>JWT: canActivate()
    JWT-->>CTRL: ✓ JWT valide (INV-239-01)

    CTRL->>RA: canActivate()
    RA-->>CTRL: ✓ Reauth token valide (INV-239-02)

    CTRL->>SVC: changePassword(userId, dto)

    SVC->>AUDIT: logEvent(PASSWORD_CHANGE_ATTEMPT)

    SVC->>REPO: findOne(userId, status=ACTIVE)
    REPO-->>SVC: user {srpSalt, passwordHash}

    SVC->>SVC: verifyOldPassword()<br/>SRP verifier + timingSafeEqual (INV-239-03)

    SVC->>KC: changeUserPassword(keycloakId, newPassword)
    KC->>KEYCLOAK: PUT /admin/realms/{realm}/users/{id}/reset-password
    KEYCLOAK-->>KC: 204 No Content
    KC-->>SVC: ✓ (INV-239-04, INV-239-08)

    SVC->>KC: log event=keycloak_password_change (INV-239-10)

    SVC->>SESS: revokeAllUserSessions(userId, PASSWORD_CHANGE)
    SESS->>REDIS: SET revocation keys (TTL 24h)
    REDIS-->>SESS: OK
    SESS-->>SVC: ✓ (INV-239-05)

    SVC->>AUDIT: logEvent(PASSWORD_CHANGE_SUCCESS)

    SVC-->>CTRL: {success: true}
    CTRL-->>C: 200 OK {success: true}

6. Mapping Invariants → Mécanismes

Invariant Mécanisme Observable
INV-239-01 OidcJwtAuthGuard (upstream, fail-closed) Logs guard, réponse 401
INV-239-02 ReauthTokenGuard (header X-Reauth-Token) Logs guard, réponse 401
INV-239-03 PasswordChangeService.verifyOldPassword() avec SRP verifier + timingSafeEqual Logs audit, réponse 400
INV-239-04 Keycloak Admin API (validation serveur) Réponse Keycloak, logs
INV-239-05 SessionRevocationStore.revokeAllUserSessions() Logs revocation, TTL 24h
INV-239-06 PasswordChangeExceptionFilter Format JSON normalisé
INV-239-07 Champ message dans erreurs + mapping Keycloak Inspection réponse
INV-239-08 KeycloakAdminService.changeUserPassword() Trace appel sortant
INV-239-09 Hors périmètre (artefact conformité) N/A
INV-239-10 Logger structuré event=keycloak_password_change Logs applicatifs

7. Mapping Critères → Tests

Critère Test Mécanisme
CA-239-01 T-239-ERR-01 OidcJwtAuthGuard
CA-239-02 T-239-ERR-02 ReauthTokenGuard
CA-239-03 T-239-ERR-03 verifyPassword()
CA-239-04 T-239-ERR-04 Keycloak policy check
CA-239-05 T-239-NOM-01 revokeAllUserSessions()
CA-239-06 T-239-ERR-05 PasswordChangeExceptionFilter
CA-239-07 T-239-ERR-03, T-239-ERR-04 Message dans réponse
CA-239-08 T-239-INV-02 Hors périmètre
CA-239-09 T-239-INV-01 Logger event=keycloak_password_change

8. Gestion des erreurs

8.1 Classes d'erreur

// password-change.errors.ts
export abstract class PasswordChangeError extends Error {
  abstract readonly code: string;
  abstract readonly httpStatus: number;
}

export class PasswordChangeUnauthenticatedError extends PasswordChangeError {
  readonly code = 'ERR-239-UNAUTHENTICATED';
  readonly httpStatus = 401;
}

export class PasswordChangeUnauthorizedReauthError extends PasswordChangeError {
  readonly code = 'ERR-239-UNAUTHORIZED-REAUTH';
  readonly httpStatus = 401;
}

export class PasswordChangeInvalidError extends PasswordChangeError {
  readonly code = 'ERR-239-PWD-INVALID';
  readonly httpStatus = 400;
}

export class PasswordChangePolicyError extends PasswordChangeError {
  readonly code = 'ERR-239-PWD-POLICY';
  readonly httpStatus = 400;
  // Message générique obligatoire (section 6 spec)
}

export class SessionInvalidationFailedError extends PasswordChangeError {
  readonly code = 'ERR-239-SESSION-INVALIDATION-FAILED';
  readonly httpStatus = 500;
}

export class PasswordChangeInternalError extends PasswordChangeError {
  readonly code = 'ERR-239-INTERNAL';
  readonly httpStatus = 500;
}

8.2 Normalisation erreurs Keycloak

Erreur Keycloak Code PD-239
400 (policy/validation) ERR-239-PWD-POLICY
401/403 (auth) ERR-239-UNAUTHORIZED-REAUTH
404 (user not found) ERR-239-PWD-INVALID
5xx (server) ERR-239-INTERNAL

8.3 Format réponse erreur

Format contractuel conforme à INV-239-06 :

{
  "error": "ERR-239-PWD-POLICY",
  "message": "Le mot de passe ne respecte pas la politique de complexité"
}

Note : Des champs supplémentaires (statusCode, timestamp, path, method) peuvent être ajoutés pour enrichir la réponse, mais les champs error et message sont obligatoires et contractuels.

9. Sécurité

9.1 Authentification

  • JWT OIDC obligatoire (OidcJwtAuthGuard, fail-closed)
  • Reauth token obligatoire (TTL 5 min, purpose="reauth")
  • Vérification sub identique entre JWT et reauth token

9.2 Protection timing attacks

  • Utilisation de crypto.timingSafeEqual() pour comparaison SRP verifier
  • Pas de distinction timing entre "user not found" et "password invalid"

9.3 Non-divulgation

  • Message ERR-239-PWD-POLICY générique (pas de détail règle)
  • Ancien et nouveau mot de passe exclus des logs (@Exclude)
  • Reauth token non loggé

9.4 Invalidation sessions

  • Toutes les sessions révoquées après succès
  • Délai max 30 secondes (INV-239-05)
  • TTL révocation 24h dans store

10. Hypothèses d'implémentation

ID Hypothèse Impact si faux
H-IMPL-01 Keycloak Admin API accessible depuis backend Endpoint non fonctionnel
H-IMPL-02 Endpoint Keycloak: PUT /admin/realms/{realm}/users/{id}/reset-password Adapter appel API
H-IMPL-03 SessionRevocationStore synchrone (< 30s) INV-239-05 non garanti
H-IMPL-04 User entity a srpSalt et passwordHash Vérification impossible

11. Points de vigilance

11.1 Atomicité partielle

Le flux n'est pas transactionnel. Si l'invalidation des sessions échoue après le changement Keycloak : - Le changement n'est PAS annulé - ERR-239-SESSION-INVALIDATION-FAILED est retourné - L'utilisateur doit re-tenter ou contacter le support

11.2 Rate limiting (recommandation)

Note : Le rate limiting n'est pas spécifié dans PD-239. Si souhaité, réutiliser le pattern MfaRateLimitGuard existant. Ceci est une recommandation de sécurité, pas une exigence contractuelle.

11.3 Audit trail

Émettre les événements : - PASSWORD_CHANGE_ATTEMPT (début) - PASSWORD_CHANGE_SUCCESS (succès) - PASSWORD_CHANGE_FAILURE (échec, avec raison) - SESSION_INVALIDATION_SUCCESS / SESSION_INVALIDATION_FAILURE

12. Tests d'intégration

Test Prérequis Action Assertion
T-INT-01 User ACTIVE, JWT valide, reauth valide POST avec bons mots de passe 200 + sessions invalidées
T-INT-02 Pas de JWT POST 401 ERR-239-UNAUTHENTICATED
T-INT-03 JWT valide, pas de reauth POST 401 ERR-239-UNAUTHORIZED-REAUTH
T-INT-04 JWT + reauth valides, mauvais ancien mdp POST 400 ERR-239-PWD-INVALID
T-INT-05 JWT + reauth valides, nouveau mdp faible POST 400 ERR-239-PWD-POLICY + message générique
T-INT-06 Mock Keycloak 5xx POST 500 ERR-239-INTERNAL

13. Livrables

Artefact Chemin
Controller src/modules/auth/password-change/password-change.controller.ts
Service src/modules/auth/password-change/password-change.service.ts
DTO src/modules/auth/password-change/password-change.dto.ts
Errors src/modules/auth/password-change/password-change.errors.ts
Filter src/modules/auth/password-change/password-change-exception.filter.ts
Guard src/modules/auth/password-change/guards/reauth-token.guard.ts
Module src/modules/auth/password-change/password-change.module.ts
Keycloak ext src/modules/auth/mfa/services/keycloak-admin.service.ts (extension)
Tests unit src/modules/auth/password-change/__tests__/*.spec.ts
Tests e2e test/auth/password-change.e2e-spec.ts