PD-27 — Plan d'implémentation
1. Découpage en composants
| Composant | Responsabilité | Fichiers impactés |
| MfaClaimsValidator | Validation des claims OIDC amr et acr | src/auth/validators/mfa-claims.validator.ts |
| DeviceTrustService | Gestion du cycle de vie de confiance des devices | src/auth/services/device-trust.service.ts |
| DeviceTrustRepository | Persistance des devices de confiance | src/auth/repositories/device-trust.repository.ts |
| DeviceFingerprintExtractor | Extraction du fingerprint depuis la requête | src/auth/extractors/device-fingerprint.extractor.ts |
| SensitiveActionGuard | Guard pour les actions sensibles (step-up MFA) | src/auth/guards/sensitive-action.guard.ts |
| SensitiveActionsRegistry | Registre des actions classées sensibles | src/auth/config/sensitive-actions.registry.ts |
| MfaRequiredGuard | Guard vérifiant l'exigence MFA selon contexte | src/auth/guards/mfa-required.guard.ts |
| AuthSessionEnricher | Enrichissement de la session avec contexte MFA/device | src/auth/middleware/auth-session-enricher.middleware.ts |
| SecurityEventEmitter | Émission d'événements de révocation | src/auth/events/security-event.emitter.ts |
2. Flux techniques
2.1 Flux nominal : Authentification avec nouveau device
┌─────────┐ ┌─────────┐ ┌──────────────┐ ┌─────────────────┐ ┌───────────────────┐
│ Client │────▶│ IdP │────▶│ ProbatioVault│────▶│ MfaClaimsValid. │────▶│ DeviceTrustService│
└─────────┘ └─────────┘ └──────────────┘ └─────────────────┘ └───────────────────┘
│ │ │ │ │
│ 1. Login │ │ │ │
│──────────────▶│ │ │ │
│ │ │ │ │
│ 2. MFA Challenge (TOTP) │ │ │
│◀──────────────│ │ │ │
│ │ │ │ │
│ 3. TOTP Code │ │ │ │
│──────────────▶│ │ │ │
│ │ │ │ │
│ 4. Token OIDC (amr:["otp"]) │ │ │
│◀──────────────│ │ │ │
│ │ │ │ │
│ 5. Request + Token + Fingerprint│ │ │
│─────────────────────────────────▶│ │ │
│ │ │ 6. Validate claims │ │
│ │ │────────────────────▶│ │
│ │ │ │ 7. Check amr/acr │
│ │ │ │────────────────────────│
│ │ │ │ OK: amr contains "otp"│
│ │ │◀────────────────────│ │
│ │ │ │ │
│ │ │ 8. Check device trust │
│ │ │─────────────────────────────────────────────▶│
│ │ │ │ 9. New device detected│
│ │ │ │ 10. MFA valid → Trust │
│ │ │◀─────────────────────────────────────────────│
│ │ │ │ │
│ 11. Session accepted │ │ │
│◀─────────────────────────────────│ │ │
2.2 Flux nominal : Authentification depuis device de confiance
┌─────────┐ ┌──────────────┐ ┌───────────────────┐
│ Client │────▶│ ProbatioVault│────▶│ DeviceTrustService│
└─────────┘ └──────────────┘ └───────────────────┘
│ │ │
│ 1. Token (amr sans "otp") + FP │
│─────────────────▶│ │
│ │ 2. Check device │
│ │─────────────────────▶│
│ │ 3. Device trusted │
│ │ (< 90 jours) │
│ │◀─────────────────────│
│ │ │
│ 4. Session accepted (pas de MFA requis)│
│◀─────────────────│ │
2.3 Flux : Action sensible (step-up MFA)
┌─────────┐ ┌──────────────┐ ┌────────────────────┐ ┌─────────┐
│ Client │────▶│ ProbatioVault│────▶│ SensitiveActionGuard│────▶│ IdP │
└─────────┘ └──────────────┘ └────────────────────┘ └─────────┘
│ │ │ │
│ 1. Action EXPORT_DATA │ │
│─────────────────▶│ │ │
│ │ 2. Is sensitive? │ │
│ │─────────────────────▶│ │
│ │ 3. Yes, check MFA │ │
│ │◀─────────────────────│ │
│ │ 4. amr sans "otp" │ │
│ │ → Step-up required │ │
│ 5. 403 + MFA_STEP_UP_REQUIRED │ │
│◀─────────────────│ │ │
│ │ │ │
│ 6. Re-auth avec MFA │ │
│────────────────────────────────────────────────────────────────▶│
│ 7. Token (amr:["otp"]) │ │
│◀────────────────────────────────────────────────────────────────│
│ │ │ │
│ 8. Retry action + nouveau token │ │
│─────────────────▶│ │ │
│ │ 9. MFA valid │ │
│ 10. Action executed │ │
│◀─────────────────│ │ │
3. Mapping invariants → mécanismes
| Invariant ID | Exigence | Mécanisme | Composant | Observable | Risque |
| I1 | Session validée par IdP | Validation JWT PD-26 en amont, aucun fallback local | JwtAuthGuard (PD-26) | Rejet systématique si token invalide/absent | Moyen : dépendance PD-26 |
| I2 | MFA obligatoire | Exigence MFA sur nouveau device (R2) + actions sensibles (R7) | MfaRequiredGuard, SensitiveActionGuard | Logs de rejet avec raison MFA_REQUIRED | Faible |
| I3 | Aucun secret MFA dans PV | Pas de champ TOTP/seed/QR dans modèles, API, logs | Architecture (absence de code) | Audit code + inspection logs/DB | Faible : par conception |
| I4 | Décision via claims OIDC | Lecture exclusive de amr/acr depuis JWT | MfaClaimsValidator | Unit tests avec tokens variés | Faible |
| I5 | Confiance conditionnée à MFA | Device trust créé uniquement si amr contient "otp" | DeviceTrustService.grantTrust() | DB : trusted_at non null ssi MFA | Faible |
4. Mapping critères d'acceptation → mécanismes
| Critère ID | Mécanisme(s) | Composant | Observable | Risque |
| CA1 | Validation amr contient "otp" + acr suffisant | MfaClaimsValidator.validate() | Réponse 200 + session créée | Faible |
| CA2 | Rejet si amr absent ou sans "otp" quand MFA requis | MfaClaimsValidator.validate() | Réponse 401/403 + code MFA_INVALID | Faible |
| CA3 | Lookup device → si inconnu, exiger MFA | DeviceTrustService.isTrusted() + MfaRequiredGuard | Log NEW_DEVICE_MFA_REQUIRED | Moyen : fingerprint stability |
| CA4 | Guard sur routes sensibles vérifiant MFA récent | SensitiveActionGuard + SensitiveActionsRegistry | Réponse 403 MFA_STEP_UP_REQUIRED | Faible |
| CA5 | Aucun endpoint/champ/log manipulant secrets MFA | Revue architecture + tests d'inspection | Grep codebase : 0 résultat pour patterns MFA secrets | Faible : par conception |
5. Mapping tests (TC-*) → mécanismes + observables
| Test ID | Référence spec | Mécanisme(s) | Point(s) d'observation | Niveau de test |
| TC-NOM-01 | R1, R2.a, I2 | MfaRequiredGuard appliqué globalement | Session acceptée ssi politique MFA respectée | Integration |
| TC-NOM-02 | R2, CA3 | DeviceTrustService.isTrusted() retourne false pour nouveau device | Log NEW_DEVICE, rejet sans MFA | Integration |
| TC-NOM-03 | R3, R3.a | DeviceFingerprintExtractor + clé composite (client_id, fingerprint) | DB : unicité du tuple, stabilité sur N requêtes | Unit + Integration |
| TC-NOM-04 | R4, R2.a, I5 | DeviceTrustService.grantTrust() après MFA valide | DB : device_trust.trusted_at non null | Integration |
| TC-NOM-05 | R5, R5.a, R5.b | DeviceTrustService.isTrusted() vérifie last_auth_at + 90d > now | Query SQL avec date IdP | Integration |
| TC-NOM-06 | R6 | SecurityEventEmitter → DeviceTrustService.revokeTrust() | DB : revoked_at set, logs événement | Integration |
| TC-NOM-07 | R7, CA4 | SensitiveActionGuard.canActivate() | Réponse 403 si amr sans "otp" | Integration |
| TC-NOM-08 | R8 | SensitiveActionsRegistry.isSensitive(action) | Config/liste consultable | Unit |
| TC-NOM-09 | R9, R10, CA1, I4 | MfaClaimsValidator.validate(token) | Retour { valid: true } si amr.includes("otp") | Unit |
| TC-NOM-10 | R11, R12, R12.a | MfaClaimsValidator.validateAcr(token) | Rejet si acr !== "urn:probatiovault:acr:mfa" | Unit |
| TC-SCOPE-01 | §6 | Absence d'endpoints recovery codes | 404 sur /api/mfa/recovery-codes | E2E |
| TC-ERR-01 | E1, R9, CA2 | MfaClaimsValidator throw si amr absent | Exception MfaClaimMissingError | Unit |
| TC-ERR-02 | E3, R12 | MfaClaimsValidator throw si acr insuffisant | Exception AcrInsufficientError | Unit |
| TC-ERR-03 | E2, R10, CA2 | MfaClaimsValidator throw si amr sans "otp" et MFA requis | Exception MfaNotSatisfiedError | Unit |
| TC-ERR-04 | E4 | SensitiveActionGuard rejette action sensible sans MFA | Réponse 403 SENSITIVE_ACTION_MFA_REQUIRED | Integration |
| TC-INV-01 | I1 | JwtAuthGuard (PD-26) en amont | Rejet 401 si token invalide | Integration |
| TC-INV-02 | I2 | Combinaison guards sur nouveau device | Rejet si amr sans "otp" | Integration |
| TC-INV-03 | I3, CA5 | Audit statique + runtime | Grep + inspection logs/DB | Security |
| TC-INV-04 | I4 | MfaClaimsValidator ne lit que amr/acr | Code review + unit tests | Unit |
| TC-INV-05 | I5 | DeviceTrustService.grantTrust() vérifie MFA | Condition if (!mfaValid) throw | Unit |
| TC-NR-01 | I4 | Idempotence de MfaClaimsValidator | N appels → même résultat | Unit |
| TC-NR-02 | R8 | Ajout action sensible n'affecte pas les autres | Tests de non-régression | Integration |
| TC-NEG-01 | I1, I4 | JwtAuthGuard rejette tokens forgés | Réponse 401 | Security |
| TC-NEG-02 | R7, E4 | SensitiveActionGuard sur token password-only | Réponse 403 | Integration |
| TC-NEG-03 | R6 | Post-révocation, MFA exigé | DB check + rejet | Integration |
6. Gestion des erreurs
| Code erreur | Situation | HTTP | Message | Observable |
MFA_CLAIM_MISSING | Claim amr absent du token | 401 | "Authentication method reference (amr) claim is required" | Log auth.mfa.claim_missing |
MFA_NOT_SATISFIED | amr ne contient pas "otp" quand MFA requis | 403 | "Multi-factor authentication required" | Log auth.mfa.not_satisfied |
ACR_INSUFFICIENT | acr présent mais ≠ urn:probatiovault:acr:mfa | 403 | "Authentication assurance level insufficient" | Log auth.acr.insufficient |
NEW_DEVICE_MFA_REQUIRED | Nouveau device détecté, MFA requis | 403 | "MFA required for new device" | Log auth.device.new_mfa_required |
SENSITIVE_ACTION_MFA_REQUIRED | Action sensible sans MFA récent | 403 | "MFA step-up required for this action" | Log auth.sensitive.step_up_required |
DEVICE_TRUST_EXPIRED | Confiance device expirée (> 90j) | 403 | "Device trust expired, MFA required" | Log auth.device.trust_expired |
DEVICE_TRUST_REVOKED | Confiance révoquée (logout/reset) | 403 | "Device trust revoked, MFA required" | Log auth.device.trust_revoked |
Structure de réponse d'erreur
interface MfaErrorResponse {
statusCode: number;
error: string;
message: string;
code: string; // Code machine (e.g., "MFA_NOT_SATISFIED")
mfaRequired: boolean; // Indique si step-up MFA nécessaire
redirectUri?: string; // URI IdP pour re-auth MFA (optionnel)
}
7. Impacts sécurité
7.1 Risques et mitigations
| Risque | Impact | Probabilité | Mitigation |
| Fingerprint spoofing | Usurpation device confiance | Moyen | Fingerprint inclut User-Agent + IP partiel + headers |
| Token replay | Réutilisation token MFA | Faible | Expiration courte + JTI unique (PD-26) |
| Bypass garde sensible | Accès action sans MFA | Faible | Décorateur obligatoire + tests exhaustifs |
| Fuite device_fingerprint | Corrélation utilisateurs | Faible | Hash du fingerprint en DB |
| Race condition trust | Grant trust sans MFA | Très faible | Transaction DB atomique |
7.2 Journalisation sécurité
| Événement | Niveau | Données loguées | Rétention |
| MFA validation success | INFO | user_id, device_id, amr_methods | 90 jours |
| MFA validation failure | WARN | user_id, device_id, reason, amr_methods | 1 an |
| Device trust granted | INFO | user_id, device_id, fingerprint_hash | 90 jours |
| Device trust revoked | WARN | user_id, device_id, revocation_reason | 1 an |
| Sensitive action blocked | WARN | user_id, action, reason | 1 an |
| Sensitive action allowed | INFO | user_id, action, mfa_timestamp | 90 jours |
- I3 : Aucun secret MFA (TOTP seed, QR, codes secours) n'est stocké/manipulé/loggé par ProbatioVault
- RGPD : device_fingerprint hashé, pas de données biométriques
- Auditabilité : Tous les événements MFA/device sont tracés (R-SP-04)
8. Hypothèses techniques
| ID | Hypothèse | Impact si faux |
| H-01 | L'IdP (Keycloak) est configuré pour inclure amr dans les tokens | Rejet systématique de toutes sessions (E1) |
| H-02 | L'IdP émet amr: ["otp"] uniquement après validation TOTP réelle | Faux positifs MFA (violation I4) |
| H-03 | Le claim acr si présent suit la convention urn:probatiovault:acr:* | Rejets inattendus ou acceptations erronées |
| H-04 | Le device_fingerprint fourni par le client est stable pour un même device | Devices "nouveaux" en boucle, UX dégradée |
| H-05 | PD-26 (validation JWT) est implémenté et fonctionnel | Prérequis non satisfait, blocage complet |
| H-06 | La référence temporelle IdP est synchronisée (NTP) | Calcul 90 jours erroné |
| H-07 | Les actions sensibles sont décorées explicitement par les développeurs | Actions sensibles non protégées |
9. Points de vigilance (risques, dette, pièges)
9.1 Risques techniques
| Risque | Description | Mitigation |
| Fingerprint instable | Changement User-Agent/IP → nouveau device | Utiliser composants stables (client_id + hash navigateur) |
| Performance DB | Lookup device trust sur chaque requête | Index composite (user_id, client_id, fingerprint_hash) |
| Migration données | Devices existants sans trust | Migration : tous devices existants → non trusted |
| Coordination IdP | Configuration Keycloak doit matcher | Documentation + tests d'intégration |
9.2 Dette technique anticipée
- Liste actions sensibles : Hardcodée initialement, prévoir config externe
- Expiration 90 jours : Valeur en constante, prévoir paramétrage
- Step-up UX : Redirection IdP basique, améliorer avec modal in-app
9.3 Pièges d'implémentation
- Ne pas confondre
amr (array) et acr (string) - Vérifier inclusion :
amr.includes("otp") et non amr === "otp" - Atomicité : Grant trust dans transaction avec validation MFA
- Timezone : Toujours UTC pour calculs d'expiration
- Logs : Ne jamais logger le contenu complet du token (secrets potentiels)
10. Hors périmètre
Les éléments suivants sont explicitement exclus de PD-27 :
| Élément | Raison | Propriétaire |
| Génération/validation secrets TOTP | Délégué à l'IdP | Keycloak |
| QR codes d'enregistrement MFA | Délégué à l'IdP | Keycloak |
| Codes de secours (backup codes) | Délégué à l'IdP (§6) | Keycloak |
| Mécanismes anti-bruteforce | Délégué à l'IdP | Keycloak |
| Seuils "tentatives infructueuses" | Délégué à l'IdP (R6 note) | Keycloak |
| Procédures de récupération compte | Hors périmètre applicatif | Support/Admin |
| Configuration initiale IdP | Prérequis infrastructure | Ops |
| Propriétés cryptographiques TOTP | Hors périmètre (RFC 6238) | Standard |
Annexe : Schéma de données
Table device_trust
CREATE TABLE device_trust (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
client_id VARCHAR(255) NOT NULL,
fingerprint_hash VARCHAR(64) NOT NULL, -- SHA-256
trusted_at TIMESTAMP WITH TIME ZONE, -- NULL si jamais MFA validé
last_auth_at TIMESTAMP WITH TIME ZONE NOT NULL,
revoked_at TIMESTAMP WITH TIME ZONE, -- NULL si actif
revocation_reason VARCHAR(50), -- 'logout_global', 'security_reset', 'admin_action'
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT uk_device_trust_user_device UNIQUE (user_id, client_id, fingerprint_hash)
);
CREATE INDEX idx_device_trust_user_lookup ON device_trust(user_id, client_id, fingerprint_hash)
WHERE revoked_at IS NULL;
Document : PD-27-plan.md Version : 1.0 Statut : Draft Dépendances : PD-23, PD-26