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 Flow → Backend API → Database
┌─────────────────┐ ┌─────────────────┐ ┌──────────┐
│ 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¶
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¶
- PostgreSQL disponible avec support transactions ACID et types ENUM.
- Node.js crypto fournit
randomUUID()etrandomInt()cryptographiquement sûrs. - class-validator disponible pour validation DTO.
- TypeORM gère les transactions via
QueryRunner. - Prefect disponible et opérationnel pour l'orchestration des flows planifiés (purge comptes expirés).
- Service d'email (US Validation email) expose une interface pour insertion dans table
email_outbox. - AuditService (existant) accepte des événements avec métadonnées hashées.
- 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¶
- Créer migration
AddUserValidationFields - Mettre à jour entity
Useravec nouveaux champs - Tests migration up/down
Phase 2 : Sécurisation endpoint¶
- Créer
ForbidPasswordGuard - Créer
RateLimitGuard(implémente US Anti-abus sur/auth/register) - Renommer champs DTO (
salt→srp_salt, etc.) - Ajouter validateur
IsHexOrBase64et support objetsrp_params - Tests unitaires DTO et Guards
Phase 3 : Logique métier¶
- Modifier
AuthService.register(): - Anti-énumération (timing + réponse)
- Transaction atomique (ROLLBACK si échec email)
- Génération token validation
- Audit events (hash PII tous chemins)
- Modifier
AuthController.register(): - Appliquer
ForbidPasswordGuardetRateLimitGuard - Réponse HTTP 200 générique
- Tests unitaires et intégration
Phase 4 : Purge automatique (Prefect)¶
- Créer
AccountPurgeControlleravec endpointPOST /auth/purge/expired - Créer
AccountPurgeServiceavec logique métier purge + traces - Créer flow Prefect
account_purge.pydansProbatioVault-infra/prefect/flows/ - Déployer le flow avec cron horaire (
0 * * * *) - Tests intégration purge (comptes + traces)
- Monitoring/alerting via Prefect events
Phase 5 : Documentation et validation¶
- Mettre à jour Swagger
- Tests e2e complets (rate limit, formats hex/base64)
- Revue sécurité
Fin du plan d'implémentation.