PD-239 — Plan d'implémentation
| 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 |
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 |