Aller au contenu

PD-23 — Plan d'implémentation


📚 Navigation User Story | Document | | | ---------- | -- | | 📋 [Spécification](PD-23-specification.md) | | | 🛠️ **Plan d'implémentation** | *(ce document)* | | ✅ [Critères d'acceptation](PD-23-acceptability.md) | *(à venir)* | | 📝 [Retour d'expérience](PD-23-rex.md) | *(à venir)* | [← Retour à auth-identity](../PD-182-epic.md) · [↑ Index User Story](index.md)

Objectif

Implémenter l'inscription utilisateur Zero-Knowledge (SRP-6a) conformément à la spécification canonique PD-23, garantissant que le mot de passe n'est jamais transmis au serveur.


1. Découpage en composants

1.1 Composants existants (à modifier)

Composant Fichier Modifications requises
RegisterDto src/modules/auth/dto/register.dto.ts Ajouter validation rejet password, renommer champs
AuthService.register() src/modules/auth/auth.service.ts Anti-énumération, atomicité, audit, token validation
AuthController.register() src/modules/auth/auth.controller.ts Réponse générique HTTP 200
User entity src/modules/auth/entities/user.entity.ts Ajouter status, validationToken, validationTokenExpiresAt

1.2 Composants à créer

Composant Fichier Responsabilité
ForbidPasswordGuard src/modules/auth/guards/forbid-password.guard.ts Rejet des payloads contenant password
RateLimitGuard src/common/guards/rate-limit.guard.ts Application rate limiting (US Anti-abus)
AccountPurgeController src/modules/auth/controllers/account-purge.controller.ts Endpoint API pour purge (appelé par Prefect)
AccountPurgeService src/modules/auth/services/account-purge.service.ts Logique métier purge comptes + traces
account_purge.py ProbatioVault-infra/prefect/flows/account_purge.py Flow Prefect planifié (cron horaire)
Migration src/database/migrations/xxx-AddUserValidationFields.ts Ajout colonnes status, validation_token, etc.

1.3 Dépendances externes (US à créer)

Composant Responsabilité Spécification
US Validation email (à créer) Flux complet validation email Envoi email synchrone dans transaction
US Anti-abus (à créer) Rate limiting, CAPTCHA Seuils, fenêtres, codes HTTP

2. Flux techniques

2.1 Flux nominal d'inscription (Outbox Pattern)

Architecture atomique : L'email est envoyé après le COMMIT pour garantir l'invariant 6 ("succès complet ou aucun effet"). Le pattern Outbox assure qu'aucun email n'est envoyé pour un compte qui n'existe pas.

┌─────────┐                              ┌─────────┐                    ┌──────────┐
│ Client  │                              │ Serveur │                    │ Database │
└────┬────┘                              └────┬────┘                    └────┬─────┘
     │                                        │                              │
     │ 1. Génère localement:                  │                              │
     │    - salt = random(16-32 bytes)        │                              │
     │    - K_auth = Argon2id(pwd, salt)      │                              │
     │    - x = SHA3-256(salt || K_auth)      │                              │
     │    - verifier = g^x mod N              │                              │
     │                                        │                              │
     │ POST /auth/register                    │                              │
     │ {email, srp_salt, srp_verifier,        │                              │
     │  srp_params}                           │                              │
     ├───────────────────────────────────────>│                              │
     │                                        │                              │
     │                           2. Valide payload (pas de `password`)       │
     │                              → Si invalide: log audit (hash requête)  │
     │                           3. Valide format email, salt, verifier      │
     │                              → Si invalide: log audit (hash requête)  │
     │                                        │                              │
     │                           4. BEGIN TRANSACTION                        │
     │                                        │                              │
     │                                        │  5. Check email exists       │
     │                                        │─────────────────────────────>│
     │                                        │<─────────────────────────────│
     │                                        │                              │
     │                           6. Si existe: timing mitigation             │
     │                              + log audit (hash email)                 │
     │                              + ROLLBACK                               │
     │                              → HTTP 200 {"status":"OK"}               │
     │                                        │                              │
     │                           7. Si nouveau:                              │
     │                              - Génère validationToken (UUID)          │
     │                              - TTL = now + 1h                         │
     │                                        │                              │
     │                                        │  8. INSERT user              │
     │                                        │     (PENDING_VALIDATION)     │
     │                                        │─────────────────────────────>│
     │                                        │<─────────────────────────────│
     │                                        │                              │
     │                                        │  9. INSERT email_outbox      │
     │                                        │     (to, template, token)    │
     │                                        │─────────────────────────────>│
     │                                        │<─────────────────────────────│
     │                                        │                              │
     │                          10. Log audit (hash identifiants)            │
     │                                        │                              │
     │                          11. COMMIT TRANSACTION                       │
     │                                        │                              │
     │                          ─────── TRANSACTION RÉUSSIE ───────          │
     │                                        │                              │
     │                          12. Traitement Outbox (post-commit):         │
     │                              - SELECT email_outbox WHERE sent=false   │
     │                              - Envoi email validation                 │
     │                              - UPDATE email_outbox SET sent=true      │
     │                              → Si échec envoi: retry async (hors tx)  │
     │                                        │                              │
     │  HTTP 200 {"status":"OK"}              │                              │
     │<───────────────────────────────────────│                              │
     │                                        │                              │

Garanties Outbox Pattern :

  • ✅ Si COMMIT réussit → le compte existe, l'email sera envoyé (au pire en retry)
  • ✅ Si ROLLBACK → le compte n'existe pas, aucun email en outbox
  • ✅ Échec email post-commit → retry async, compte reste valide

2.2 Flux purge comptes non validés (INV-10, INV-12)

Architecture : Prefect FlowBackend APIDatabase

┌─────────────────┐            ┌─────────────────┐            ┌──────────┐
│ Prefect Flow    │            │ Backend API     │            │ Database │
│ account_purge   │            │ AccountPurge    │            │          │
│ (cron: 0 * * *) │            │ Controller      │            │          │
└────────┬────────┘            └────────┬────────┘            └────┬─────┘
         │                              │                          │
         │  POST /auth/purge/expired    │                          │
         ├─────────────────────────────>│                          │
         │                              │                          │
         │                              │ 1. SELECT users WHERE    │
         │                              │    status=PENDING        │
         │                              │    AND token_expires<NOW │
         │                              ├─────────────────────────>│
         │                              │<─────────────────────────│
         │                              │                          │
         │                              │ 2. Pour chaque user:     │
         │                              │    a. hash = SHA256(email)│
         │                              │    b. DELETE audit_logs  │
         │                              │       WHERE user_id OR   │
         │                              │       hashed_email=hash  │
         │                              ├─────────────────────────>│
         │                              │<─────────────────────────│
         │                              │    c. DELETE user        │
         │                              ├─────────────────────────>│
         │                              │<─────────────────────────│
         │                              │    d. Log ACCOUNT_PURGED │
         │                              │                          │
         │  {"purged": N, "status":"OK"}│                          │
         │<─────────────────────────────│                          │
         │                              │                          │
         │ 3. Emit Prefect event        │                          │
         │    account.purge.{env}       │                          │
         │                              │                          │

Déploiement Prefect :

prefect deployment build account_purge.py:account_purge_flow \
    -n "account-purge-{env}" \
    --cron "0 * * * *" \
    -q default


2.3 Diagrammes Mermaid

Graphe de dépendances des composants

graph TD
    subgraph "Controllers"
        AC["AuthController<br/>/auth/register"]
        APC["AccountPurgeController<br/>/auth/purge/expired"]
    end

    subgraph "Guards"
        FPG["ForbidPasswordGuard"]
        RLG["RateLimitGuard"]
        AAG["AdminAuthGuard"]
    end

    subgraph "Services"
        AS["AuthService.register()"]
        APS["AccountPurgeService"]
        AuS["AuditService"]
    end

    subgraph "DTO / Validation"
        RD["RegisterDto"]
        SPD["SrpParamsObjectDto"]
        HOB["IsHexOrBase64 validator"]
    end

    subgraph "Persistence"
        UE["User Entity<br/>(vault_secure.users)"]
        EO["email_outbox table"]
        MIG["Migration<br/>AddUserValidationFields"]
    end

    subgraph "External"
        PF["Prefect Flow<br/>account_purge.py"]
        EMAIL["Service Email<br/>(US à créer)"]
        ANTIABUS["Rate Limiting<br/>(US à créer)"]
    end

    AC -->|"@UseGuards"| FPG
    AC -->|"@UseGuards"| RLG
    AC -->|"appelle"| AS
    APC -->|"@UseGuards"| AAG
    APC -->|"appelle"| APS

    RD -->|"valide"| HOB
    RD -->|"@ValidateNested"| SPD
    AC -->|"@Body"| RD

    AS -->|"INSERT user"| UE
    AS -->|"INSERT outbox"| EO
    AS -->|"logEvent()"| AuS
    AS -->|"timing mitigation"| AS

    APS -->|"SELECT expired"| UE
    APS -->|"DELETE user"| UE
    APS -->|"purgeByHash*()"| AuS
    APS -->|"logEvent()"| AuS

    PF -->|"POST /auth/purge/expired"| APC
    EO -->|"post-commit"| EMAIL
    RLG -->|"délègue seuils"| ANTIABUS
    MIG -->|"ALTER TABLE"| UE

    style FPG fill:#f9d6d6,stroke:#c00
    style RLG fill:#f9d6d6,stroke:#c00
    style AAG fill:#f9d6d6,stroke:#c00
    style AuS fill:#d6f9d6,stroke:#090
    style PF fill:#d6d6f9,stroke:#009
    style EMAIL fill:#ffd,stroke:#990
    style ANTIABUS fill:#ffd,stroke:#990

Diagramme de séquence — Inscription multi-services

sequenceDiagram
    participant C as Client
    participant Ctrl as AuthController
    participant FPG as ForbidPasswordGuard
    participant RLG as RateLimitGuard
    participant Svc as AuthService
    participant DB as PostgreSQL<br/>(vault_secure)
    participant Audit as AuditService
    participant Outbox as email_outbox
    participant Email as Service Email

    C->>+Ctrl: POST /auth/register<br/>{email, srp_salt, srp_verifier, srp_params}

    Ctrl->>+FPG: canActivate(request)
    alt payload contient "password"
        FPG-->>Ctrl: BadRequestException
        Ctrl-->>C: 400 FORBIDDEN_FIELD
    else OK
        FPG-->>-Ctrl: true
    end

    Ctrl->>+RLG: canActivate(request)
    alt rate limit dépassé
        RLG-->>Ctrl: 429 RATE_LIMITED
        Ctrl-->>C: 429 RATE_LIMITED
    else OK
        RLG-->>-Ctrl: true
    end

    Ctrl->>+Svc: register(RegisterDto)

    Note over Svc,DB: BEGIN TRANSACTION

    Svc->>+DB: SELECT user WHERE email = ?
    DB-->>-Svc: user | null

    alt email existe déjà
        Svc->>Svc: timingAttackMitigation()<br/>(50-150ms aléatoire)
        Svc->>Audit: logEvent(REGISTRATION_DUPLICATE,<br/>SHA256(email))
        Note over Svc,DB: ROLLBACK
        Svc-->>Ctrl: {status: "OK"}
    else nouvel utilisateur
        Svc->>Svc: validationToken = randomUUID()<br/>TTL = now + 1h
        Svc->>+DB: INSERT user (PENDING_VALIDATION)
        DB-->>-Svc: OK
        Svc->>+Outbox: INSERT email_outbox (to, template, token)
        Outbox-->>-Svc: OK
        Svc->>Audit: logEvent(REGISTRATION_SUCCESS,<br/>SHA256(userId), SHA256(email))
        Note over Svc,DB: COMMIT
        Svc-->>Ctrl: {status: "OK"}
    end

    Ctrl-->>-C: 200 {status: "OK"}

    Note over Outbox,Email: Post-commit (async)
    Outbox->>+Email: Envoi email validation
    Email-->>-Outbox: OK / retry

3. Mapping invariants → mécanismes

# Invariant Mécanisme technique Vérification
1 Mot de passe jamais transmis ForbidPasswordGuard rejette payload avec password Test unitaire + e2e
2 Serveur ne peut restituer le mot de passe Seul srp_verifier stocké (dérivé irréversible) Audit code, test
3 Seuls artefacts SRP persistés Entity User : srpSalt, passwordHash (verifier), srpVersion Schema DB, test
4 Email unique Contrainte UNIQUE en DB + validation Migration, test
5 Validation avant persistance class-validator sur RegisterDto Test unitaire
6 Création atomique Transaction PostgreSQL avec ROLLBACK Test intégration
7 Traçabilité sans PII AuditService.logEvent() avec SHA256(email) Test, audit logs
8 Anti-énumération Réponse identique (timing + body) email existant/nouveau Test e2e timing
9 État PENDING_VALIDATION Colonne status enum, valeur initiale Migration, test
10 Purge automatique Flow Prefect account_purge (cron horaire) → API /auth/purge/expired Test intégration
11 Anti-automatisation @UseGuards(RateLimitGuard) sur POST /auth/register (US Anti-abus) Test e2e rate limit
12 Conformité RGPD Hash PII tous chemins (succès, erreurs, logs), purge traces avec compte Test, audit

4. Contrat d'API détaillé

4.1 Endpoint

POST /auth/register
Content-Type: application/json

4.2 Payload d'entrée

interface RegisterRequest {
  email: string;           // Format RFC 5322, max 254 chars
  srp_salt: string;        // Hex OU Base64, 16-32 bytes encodés
  srp_verifier: string;    // Hex OU Base64, variable (selon groupe SRP)
  srp_params?: string | SrpParamsObject;  // Identifiant OU objet paramètres
  client_metadata?: {      // Optionnel, sans PII
    client_version?: string;
    platform?: string;
  };
}

// Forme objet pour srp_params
interface SrpParamsObject {
  group: string;           // "3072" | "4096" | ...
  hash?: string;           // "SHA3-256" (default)
  kdf?: string;            // "Argon2id" (default)
}

4.3 Validation DTO

// src/modules/auth/dto/register.dto.ts

/**
 * Validateur custom pour hex OU base64
 */
function IsHexOrBase64() {
  return function (object: object, propertyName: string) {
    registerDecorator({
      name: 'isHexOrBase64',
      target: object.constructor,
      propertyName: propertyName,
      validator: {
        validate(value: unknown) {
          if (typeof value !== 'string') return false;
          // Hex: uniquement [0-9a-fA-F]
          const isHex = /^[0-9a-fA-F]+$/.test(value);
          // Base64: [A-Za-z0-9+/=]
          const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(value);
          return isHex || isBase64;
        },
        defaultMessage() {
          return '$property must be hexadecimal or base64 encoded';
        },
      },
    });
  };
}

/**
 * DTO pour srp_params sous forme d'objet
 *
 * NOTE: Les valeurs supportées ci-dessous sont les valeurs INITIALES
 * de l'implémentation. La spécification indique que les paramètres SRP
 * canoniques sont "à clarifier" (§11 point 3). Ces valeurs peuvent être
 * étendues lors de la clarification sans considérer cela comme un breaking change.
 *
 * @see PD-23-specification.md §11 "Points à clarifier"
 */
export class SrpParamsObjectDto {
  // Valeurs initiales supportées - extensible après clarification spec
  @IsIn(['3072', '4096'])
  group: string;

  @IsOptional()
  @IsIn(['SHA3-256', 'SHA-256'])  // Extensible après clarification
  hash?: string;

  @IsOptional()
  @IsIn(['Argon2id'])  // Extensible après clarification
  kdf?: string;
}

export class RegisterDto {
  @IsEmail()
  @MaxLength(254)
  email: string;

  @IsHexOrBase64()
  @IsNotEmpty()
  @ApiProperty({ name: 'srp_salt', description: 'Hex ou Base64, 16-32 bytes' })
  srpSalt: string;

  @IsHexOrBase64()
  @IsNotEmpty()
  @ApiProperty({ name: 'srp_verifier', description: 'Hex ou Base64' })
  srpVerifier: string;

  @IsOptional()
  @ValidateIf((o) => typeof o.srpParams === 'string')
  @IsIn(['3072', '4096'])
  @ValidateIf((o) => typeof o.srpParams === 'object')
  @ValidateNested()
  @Type(() => SrpParamsObjectDto)
  @ApiProperty({ name: 'srp_params', description: 'Identifiant string ou objet paramètres' })
  srpParams?: string | SrpParamsObjectDto;

  @IsOptional()
  @ValidateNested()
  @Type(() => ClientMetadataDto)
  @ApiProperty({ name: 'client_metadata' })
  clientMetadata?: ClientMetadataDto;
}

4.4 Réponses

Cas HTTP Body Notes
Succès (nouvel utilisateur) 200 {"status":"OK"} Email validation déclenché
Email déjà existant 200 {"status":"OK"} Indiscernable du succès
Payload contient password 400 {"error":"FORBIDDEN_FIELD","field":"password"} Avant toute autre validation
Validation échouée 400 {"error":"VALIDATION_ERROR","details":[...]} class-validator errors
Rate limit dépassé 429 {"error":"RATE_LIMITED"} Défini par US Anti-abus
Erreur interne 500 {"error":"INTERNAL_ERROR"} Générique, tracé en interne

5. Modifications de schéma

5.1 Migration : AddUserValidationFields

// src/database/migrations/XXXXXX-AddUserValidationFields.ts

export class AddUserValidationFields implements MigrationInterface {
  async up(queryRunner: QueryRunner): Promise<void> {
    // Enum pour status
    await queryRunner.query(`
      CREATE TYPE vault_secure.user_status AS ENUM (
        'PENDING_VALIDATION',
        'ACTIVE',
        'SUSPENDED',
        'DELETED'
      );
    `);

    // Colonnes
    await queryRunner.query(`
      ALTER TABLE vault_secure.users
      ADD COLUMN status vault_secure.user_status
        NOT NULL DEFAULT 'PENDING_VALIDATION',
      ADD COLUMN validation_token UUID,
      ADD COLUMN validation_token_expires_at TIMESTAMPTZ,
      ADD COLUMN email_validated_at TIMESTAMPTZ;
    `);

    // Index pour purge
    await queryRunner.query(`
      CREATE INDEX idx_users_pending_validation
      ON vault_secure.users (validation_token_expires_at)
      WHERE status = 'PENDING_VALIDATION';
    `);
  }

  async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      ALTER TABLE vault_secure.users
      DROP COLUMN email_validated_at,
      DROP COLUMN validation_token_expires_at,
      DROP COLUMN validation_token,
      DROP COLUMN status;
    `);
    await queryRunner.query(`DROP TYPE vault_secure.user_status;`);
  }
}

5.2 Entity User mise à jour

// Ajouts à src/modules/auth/entities/user.entity.ts

export enum UserStatus {
  PENDING_VALIDATION = 'PENDING_VALIDATION',
  ACTIVE = 'ACTIVE',
  SUSPENDED = 'SUSPENDED',
  DELETED = 'DELETED',
}

@Entity('users', { schema: 'vault_secure' })
export class User {
  // ... champs existants ...

  @Column({
    type: 'enum',
    enum: UserStatus,
    default: UserStatus.PENDING_VALIDATION,
  })
  status: UserStatus;

  @Column({ name: 'validation_token', type: 'uuid', nullable: true })
  validationToken?: string;

  @Column({ name: 'validation_token_expires_at', type: 'timestamptz', nullable: true })
  validationTokenExpiresAt?: Date;

  @Column({ name: 'email_validated_at', type: 'timestamptz', nullable: true })
  emailValidatedAt?: Date;
}

6. Gestion des erreurs

6.1 Matrice erreurs → réponses

Invariant 7 : "Toute tentative d'inscription est traçable via des événements minimisés, sans PII en clair."

Erreur technique Code HTTP Réponse client Action interne
Champ password présent 400 FORBIDDEN_FIELD Log audit REGISTRATION_FORBIDDEN_FIELD (SHA256(IP), timestamp)
Email format invalide 400 VALIDATION_ERROR Log audit REGISTRATION_VALIDATION_ERROR (SHA256(IP), errorType="email_invalid", timestamp)
Salt/Verifier invalide 400 VALIDATION_ERROR Log audit REGISTRATION_VALIDATION_ERROR (SHA256(IP), errorType="srp_invalid", timestamp)
Email déjà existant 200 {"status":"OK"} Log audit REGISTRATION_DUPLICATE (SHA256(email), timestamp), timing mitigation
Échec INSERT DB 500 INTERNAL_ERROR Log error (SHA256(email)), ROLLBACK
Échec génération token 500 INTERNAL_ERROR Log error, ROLLBACK
Échec INSERT outbox 500 INTERNAL_ERROR Log error (SHA256(email)), ROLLBACK
Rate limit 429 RATE_LIMITED Log audit REGISTRATION_RATE_LIMITED (SHA256(IP), timestamp)

Note : Les erreurs de validation (email invalide, SRP invalide) sont tracées avec l'IP hashée uniquement, sans aucune donnée du payload (conformité RGPD + anti-énumération).

6.2 Timing attack mitigation

// Dans AuthService.register()

private async timingAttackMitigation(): Promise<void> {
  // Délai aléatoire 50-150ms pour masquer la différence
  // entre email existant et nouveau
  const delay = randomInt(50, 150);
  await new Promise(resolve => setTimeout(resolve, delay));
}

async register(dto: RegisterDto): Promise<{ status: string }> {
  const existingUser = await this.userRepository.findOne({
    where: { email: dto.email }
  });

  if (existingUser) {
    // Simuler le temps d'une vraie insertion
    await this.timingAttackMitigation();
    // Log audit avec hash
    await this.auditService.logEvent(
      'REGISTRATION_DUPLICATE',
      'anonymous',
      this.hashEmail(dto.email),
      { timestamp: new Date().toISOString() }
    );
    // Réponse identique
    return { status: 'OK' };
  }

  // ... création normale ...
}

7. Impacts sécurité

7.1 Analyse des risques

Risque Mitigation Invariant
Transmission mot de passe Guard rejet password, architecture SRP INV-1
Brute-force verifier Verifier = g^x mod N irréversible, rate limiting INV-2, INV-11
Énumération emails Réponse indiscernable, timing constant INV-8
Injection SQL TypeORM paramétré, validation DTO INV-5
Stockage PII logs Hash SHA256 des emails INV-7, INV-12
Comptes orphelins Purge automatique 1h INV-10

7.2 Audit events

Invariant 12 : "Aucune PII en clair dans les logs". L'identifiant utilisateur (UUID) est considéré comme PII car il permet l'identification indirecte.

Event Données loggées Exclusions
REGISTRATION_SUCCESS SHA256(userId), SHA256(email), timestamp userId clair, email clair
REGISTRATION_DUPLICATE SHA256(email), timestamp email clair
REGISTRATION_VALIDATION_ERROR SHA256(IP), errorType, timestamp payload, email, IP clair
REGISTRATION_FORBIDDEN_FIELD SHA256(IP), fieldName, timestamp payload, email, IP clair
REGISTRATION_RATE_LIMITED SHA256(IP), timestamp email, IP clair
ACCOUNT_PURGED SHA256(userId), SHA256(email), timestamp userId clair, email clair
ACCOUNT_PURGE_BATCH count, timestamp user details

Justification hash userId : Même si un UUID v4 est pseudo-aléatoire, il constitue un identifiant unique persistant permettant de relier des actions à une personne physique. La minimisation RGPD impose le hash dans les logs d'audit accessibles.

7.3 Purge traces RGPD (INV-12)

Lors de la suppression d'un compte non validé, les traces d'audit associées doivent être purgées :

// Dans AccountPurgeService

async purgeExpiredAccounts(): Promise<void> {
  const expiredUsers = await this.userRepository.find({
    where: {
      status: UserStatus.PENDING_VALIDATION,
      validationTokenExpiresAt: LessThan(new Date()),
    },
  });

  for (const user of expiredUsers) {
    const hashedEmail = this.hashPii(user.email);
    const hashedUserId = this.hashPii(user.id);

    // 1. Purger les traces d'audit liées à cet utilisateur (par hash)
    await this.auditService.purgeByHashedUserId(hashedUserId);
    await this.auditService.purgeByHashedEmail(hashedEmail);

    // 2. Supprimer l'utilisateur
    await this.userRepository.delete(user.id);

    // 3. Log minimal de purge (sans PII - tous identifiants hashés)
    await this.auditService.logEvent(
      'ACCOUNT_PURGED',
      'system',
      { hashedUserId, hashedEmail, timestamp: new Date().toISOString() }
    );
  }
}

private hashPii(value: string): string {
  return createHash('sha256').update(value).digest('hex');
}

8. Tests requis

8.1 Tests unitaires

Test Fichier Couverture
RegisterDto validation register.dto.spec.ts Tous les champs, cas limites
ForbidPasswordGuard forbid-password.guard.spec.ts Rejet si password présent
AuthService.register auth.service.spec.ts Flux nominal, duplicat, erreurs
AccountPurgeService account-purge.service.spec.ts Sélection, suppression, logs

8.2 Tests intégration

Test Description
Transaction atomique Vérifier ROLLBACK si échec partiel
Anti-énumération Comparer timing email existant vs nouveau
Purge automatique Créer compte, attendre expiration, vérifier suppression

8.3 Tests e2e

Scénario Expected
Inscription valide HTTP 200, user créé PENDING_VALIDATION
Payload avec password HTTP 400 FORBIDDEN_FIELD
Email déjà existant HTTP 200 identique (anti-énumération)
Validation token expirée Compte purgé par cron

9. Hypothèses techniques

  1. PostgreSQL disponible avec support transactions ACID et types ENUM.
  2. Node.js crypto fournit randomUUID() et randomInt() cryptographiquement sûrs.
  3. class-validator disponible pour validation DTO.
  4. TypeORM gère les transactions via QueryRunner.
  5. Prefect disponible et opérationnel pour l'orchestration des flows planifiés (purge comptes expirés).
  6. Service d'email (US Validation email) expose une interface pour insertion dans table email_outbox.
  7. AuditService (existant) accepte des événements avec métadonnées hashées.
  8. TLS obligatoire en production (hors périmètre applicatif).

9.1 Fallback en cas d'indisponibilité Prefect

Si le flow Prefect account_purge n'est pas déployé ou est indisponible, la conformité aux invariants 10 et 12 (purge automatique) est compromise. Les mécanismes de fallback suivants sont prévus :

Situation Détection Fallback
Flow non déployé Healthcheck /auth/purge/status retourne { "prefect": false } Alerte monitoring + purge manuelle
Flow en échec répété Prefect event account.purge.failed Alerte Ops + intervention manuelle
Prefect injoignable Timeout sur appel API Prefect Endpoint manuel POST /auth/purge/expired avec authentification admin

Endpoint de purge manuelle (fallback uniquement) :

@Post('purge/expired')
@UseGuards(AdminAuthGuard)  // Authentification admin requise
async manualPurge(): Promise<{ purged: number }> {
  return this.accountPurgeService.purgeExpiredAccounts();
}

Monitoring obligatoire : L'absence de flow Prefect doit déclencher une alerte dans les 24h pour garantir la conformité RGPD.


10. Points de vigilance

10.1 Critiques

Point Risque Action
Timing attack Fuite existence email Délai aléatoire obligatoire
Transaction incomplète User créé sans token ROLLBACK explicite
Logs PII Non-conformité RGPD Hash systématique

10.2 Coordination inter-specs

Dépendance Impact
US Validation email (à créer) Génération token, envoi email, endpoint validation
US Anti-abus (à créer) Rate limiting avant validation DTO

10.3 Écarts implémentation existante

L'implémentation actuelle présente les écarts suivants à corriger :

Écart Actuel Cible
Anti-énumération ConflictException si email existe HTTP 200 identique
Rejet password Non implémenté Guard obligatoire
Status utilisateur Absent PENDING_VALIDATION par défaut
Token validation Absent UUID avec TTL 1h
Purge automatique Absente CRON horaire
Réponse HTTP 201 avec userId 200 avec {"status":"OK"}
Audit inscription Absent Event avec hash email
Noms champs DTO salt, verifier srp_salt, srp_verifier

11. Découpage tâches

Phase 1 : Préparation base de données

  1. Créer migration AddUserValidationFields
  2. Mettre à jour entity User avec nouveaux champs
  3. Tests migration up/down

Phase 2 : Sécurisation endpoint

  1. Créer ForbidPasswordGuard
  2. Créer RateLimitGuard (implémente US Anti-abus sur /auth/register)
  3. Renommer champs DTO (saltsrp_salt, etc.)
  4. Ajouter validateur IsHexOrBase64 et support objet srp_params
  5. Tests unitaires DTO et Guards

Phase 3 : Logique métier

  1. Modifier AuthService.register() :
  2. Anti-énumération (timing + réponse)
  3. Transaction atomique (ROLLBACK si échec email)
  4. Génération token validation
  5. Audit events (hash PII tous chemins)
  6. Modifier AuthController.register() :
  7. Appliquer ForbidPasswordGuard et RateLimitGuard
  8. Réponse HTTP 200 générique
  9. Tests unitaires et intégration

Phase 4 : Purge automatique (Prefect)

  1. Créer AccountPurgeController avec endpoint POST /auth/purge/expired
  2. Créer AccountPurgeService avec logique métier purge + traces
  3. Créer flow Prefect account_purge.py dans ProbatioVault-infra/prefect/flows/
  4. Déployer le flow avec cron horaire (0 * * * *)
  5. Tests intégration purge (comptes + traces)
  6. Monitoring/alerting via Prefect events

Phase 5 : Documentation et validation

  1. Mettre à jour Swagger
  2. Tests e2e complets (rate limit, formats hex/base64)
  3. Revue sécurité

Fin du plan d'implémentation.