Aller au contenu

đŸ› ïž PLAN D'IMPLÉMENTATION


📚 Navigation User Story | Document | | | ---------- | -- | | 📋 SpĂ©cification | [PD-25-specification.md](PD-25-specification.md) | | đŸ› ïž **Plan d'implĂ©mentation** | *(ce document)* | | ✅ CritĂšres d'acceptation | [PD-25-acceptability.md](PD-25-acceptability.md) | | 📝 Retour d'expĂ©rience | [PD-25-rex.md](PD-25-rex.md) | [← Retour Ă  crypto-proof](../PD-189-epic.md) · [↑ Index User Story](index.md)

PD-25 — Authentification SRP-6a (Phase 2 : preuve, clĂ© de session, token)

📌 EPIC âžĄïž PD-189 — CRYPTO

📌 Artefact produit âžĄïž PD-25-plan.md


1. Découpage en composants

1.1 Composants existants (PD-24)

Composant Fichier RĂŽle
SrpService src/modules/auth/services/srp.service.ts Opérations cryptographiques SRP-6a
SrpSessionStoreService src/modules/auth/services/srp-session-store.service.ts Stockage Redis des sessions SRP
AuthService src/modules/auth/auth.service.ts Orchestration authentification
AuthController src/modules/auth/auth.controller.ts API endpoints /auth/*
LoginVerifyDto src/modules/auth/dto/login-verify.dto.ts Validation entrée Phase 2

1.2 Composants Ă  enrichir (PD-25)

Composant Modification Justification
AuthService Ajout traçabilité audit INV-7 : Toute tentative traçable
SrpService Documentation sessionKey non-persistance INV-4 : Clé jamais persistée
JwtModule Configuration claims JWT Point à clarifier §10.3

1.3 Nouveaux composants

Composant Fichier proposé RÎle
SrpAuditEvents src/modules/auth/events/srp-audit.events.ts ÉvĂ©nements d'audit SRP

2. Flux techniques

2.1 Diagramme de séquence Phase 2

┌────────┐                           ┌────────┐                    ┌───────┐
│ Client │                           │ Serveur│                    │ Redis │
└───┬────┘                           └───┬────┘                    └───┬───┘
    │                                    │                             │
    │  POST /auth/login/verify           │                             │
    │  { email, M1 }                     │                             │
    │───────────────────────────────────â–ș│                             │
    │                                    │                             │
    │                                    │  GET srp:session:{email}    │
    │                                    │────────────────────────────â–ș│
    │                                    │                             │
    │                                    │  { A, b, B, salt, verifier }│
    │                                    │◄────────────────────────────│
    │                                    │                             │
    │                  ┌─────────────────┮─────────────────┐           │
    │                  │  ══ PHASE VÉRIFICATION ══         │           │
    │                  │  (K est calculĂ©e mais NON Ă©tablie)│           │
    │                  │                                   │           │
    │                  │ 1. Calculer u = SHA3-256(A || B)  │           │
    │                  │ 2. Calculer S = (A * v^u)^b mod N │           │
    │                  │ 3. Calculer K' = SHA3-256(S)      │           │
    │                  │    ⚠  K' est une valeur candidate │           │
    │                  │ 4. Calculer M1' = SHA3-256(A||B||K')          │
    │                  │ 5. VĂ©rifier M1' === M1            │           │
    │                  │    ❌ Si Ă©chec → K' est REJETÉE   │           │
    │                  └─────────────────┬─────────────────┘           │
    │                                    │                             │
    │                                    │  [Si M1 invalide → 401]     │
    │                                    │  K' n'est jamais Ă©tablie    │
    │                                    │                             │
    │                  ┌─────────────────┮─────────────────┐           │
    │                  │  ══ PHASE POST-VÉRIFICATION ══    │           │
    │                  │  (K est maintenant ÉTABLIE)       │           │
    │                  │                                   │           │
    │                  │ 6. K := K' (clĂ© de session valide)│           │
    │                  │ 7. Calculer M2 = SHA3-256(A||M1||K)│          │
    │                  │ 8. GĂ©nĂ©rer JWT (email, sub)       │           │
    │                  │ 9. Émettre Ă©vĂ©nement audit SUCCESS│           │
    │                  └─────────────────┬─────────────────┘           │
    │                                    │                             │
    │                                    │  DEL srp:session:{email}    │
    │                                    │────────────────────────────â–ș│
    │                                    │                             │
    │  { accessToken, M2 }               │                             │
    │◄───────────────────────────────────│                             │
    │                                    │                             │
    │  [Client vĂ©rifie M2]               │                             │
    │                                    │                             │

Note importante (INV-3) : Le protocole SRP-6a nécessite de calculer K' pour vérifier M1 (car M1 = SHA3-256(A||B||K)). Cependant, K' reste une valeur candidate jusqu'à ce que M1 soit validé. La clé de session K n'est établie (i.e., considérée valide et utilisable) qu'aprÚs vérification réussie de M1. En cas d'échec, K' est rejetée et jamais retournée.

2.2 Flux détaillé loginVerify()

// auth.service.ts:199-245
async loginVerify(loginVerifyDto: LoginVerifyDto): Promise<{ accessToken: string; M2: string }> {
  // 1. Récupérer session SRP depuis Redis
  const session = await this.srpSessionStore.get(email);

  // 2. Vérifier TTL (double sécurité avec Redis)
  if (Date.now() - session.createdAt > TTL) → 401

  // 3. Vérifier preuve client (SrpService.verifyClientProof)
  //    - Calcul u, S, K
  //    - Vérification M1
  //    - Génération M2
  const { M2 } = this.srpService.verifyClientProof(...);

  // 4. Supprimer session (usage unique)
  await this.srpSessionStore.delete(email);

  // 5. Générer JWT
  const accessToken = this.jwtService.sign({ email, sub: userId });

  // 6. [À AJOUTER] Émettre Ă©vĂ©nement audit

  return { accessToken, M2 };
}

2.3 Diagrammes Mermaid

2.3.1 Graphe de dépendances des composants

graph TD
    subgraph "API Layer"
        AC[AuthController<br/><code>auth.controller.ts</code>]
    end

    subgraph "Service Layer"
        AS[AuthService<br/><code>auth.service.ts</code>]
        SS[SrpService<br/><code>srp.service.ts</code>]
        JWT[JwtModule<br/><code>@nestjs/jwt</code>]
        AUD[AuditService<br/><code>audit.service.ts</code>]
    end

    subgraph "Data Layer"
        SSS[SrpSessionStoreService<br/><code>srp-session-store.service.ts</code>]
        REDIS[(Redis<br/>sessions SRP)]
        DB[(PostgreSQL<br/>users)]
    end

    subgraph "DTO"
        LVD[LoginVerifyDto<br/><code>login-verify.dto.ts</code>]
    end

    AC -->|"POST /auth/login/verify"| AS
    AC -.->|validation| LVD
    AS -->|"verifyClientProof()"| SS
    AS -->|"sign(payload)"| JWT
    AS -->|"log(event)"| AUD
    AS -->|"get/delete session"| SSS
    SSS -->|"GET/DEL srp:session:*"| REDIS
    AS -->|"findOne(email)"| DB

    style SS fill:#f9e6e6,stroke:#c0392b,color:#000
    style AUD fill:#e6f9e6,stroke:#27ae60,color:#000
    style REDIS fill:#e6e6f9,stroke:#2980b9,color:#000

2.3.2 Diagramme de sĂ©quence — Phase 2 (multi-service)

sequenceDiagram
    participant C as Client
    participant AC as AuthController
    participant AS as AuthService
    participant SSS as SrpSessionStoreService
    participant R as Redis
    participant SS as SrpService
    participant JWT as JwtModule
    participant AUD as AuditService

    C->>AC: POST /auth/login/verify { email, M1 }
    AC->>AS: loginVerify(loginVerifyDto)

    AS->>SSS: get(email)
    SSS->>R: GET srp:session:{email}
    R-->>SSS: { A, b, B, salt, verifier, createdAt }
    SSS-->>AS: session

    alt Session inexistante
        AS->>AUD: log(AUTH_SRP_VERIFY_FAILURE, SESSION_NOT_FOUND)
        AS-->>AC: throw 401
        AC-->>C: 401 Unauthorized
    end

    alt Session expirée (TTL > 5 min)
        AS->>SSS: delete(email)
        SSS->>R: DEL srp:session:{email}
        AS->>AUD: log(AUTH_SRP_VERIFY_FAILURE, SESSION_EXPIRED)
        AS-->>AC: throw 401
        AC-->>C: 401 Unauthorized
    end

    AS->>SS: verifyClientProof(salt, verifier, A, B, b, M1)
    Note over SS: 1. u = SHA3-256(A || B)<br/>2. S = (A * v^u)^b mod N<br/>3. K' = SHA3-256(S)<br/>4. M1' = SHA3-256(A||B||K')<br/>5. M1' === M1 ?

    alt M1 invalide
        SS-->>AS: throw Error("Preuve client invalide")
        AS->>SSS: delete(email)
        SSS->>R: DEL srp:session:{email}
        AS->>AUD: log(AUTH_SRP_VERIFY_FAILURE, Preuve invalide)
        AS-->>AC: throw 401
        AC-->>C: 401 Unauthorized
    end

    SS-->>AS: { M2 }
    Note over SS: K := K' (clé établie)<br/>M2 = SHA3-256(A||M1||K)

    AS->>SSS: delete(email)
    SSS->>R: DEL srp:session:{email}
    Note over SSS: Session usage unique

    AS->>JWT: sign({ email, sub: userId })
    JWT-->>AS: accessToken

    AS->>AUD: log(AUTH_SRP_VERIFY_SUCCESS)
    AS-->>AC: { accessToken, M2 }
    AC-->>C: 200 { accessToken, M2 }

    Note over C: Vérification M2<br/>(auth mutuelle)

3. Mapping invariants → mĂ©canismes

# Invariant Mécanisme technique Fichier Ligne
INV-1 Serveur ne connaßt jamais le mot de passe Architecture Zero-Knowledge : seul verifier stocké, dérivé cÎté client srp.service.ts Conception
INV-2 Auth réussie = preuve SRP valide Comparaison M1 === M1' avant toute émission JWT srp.service.ts 263
INV-3 Clé de session dérivée aprÚs vérification K' calculée comme candidat, retournée comme sessionKey uniquement si M1 valide (sinon exception) srp.service.ts 263-271
INV-4 Clé de session jamais persistée Variable locale K dans scope fonction, non stockée en base/Redis srp.service.ts 256
INV-5 JWT émis aprÚs auth SRP réussie jwtService.sign() appelé aprÚs verifyClientProof() succÚs auth.service.ts 230
INV-6 Aucun élément ne permet récupération secret Protocole SRP-6a : seuls salt, verifier, A, B, M1, M2 échangés RFC 5054
INV-7 Tentatives traçables Intégration AuditService avec événements AUTH_SRP_* auth.service.ts §3.1

3.1 ImplĂ©mentation INV-7 (TraçabilitĂ©) — OBLIGATOIRE

Objectif : Tracer toutes les tentatives d'authentification SRP (succÚs et échecs).

Fichiers Ă  modifier :

  1. src/modules/auth/auth.module.ts — Importer AuditModule
  2. src/modules/auth/auth.service.ts — Injecter et appeler AuditService

Étape 1 : Mise à jour du module

// auth.module.ts
import { AuditModule } from '../audit/audit.module';

@Module({
  imports: [
    // ... existant
    AuditModule,  // AJOUTER
  ],
  // ...
})
export class AuthModule {}

Étape 2 : Injection dans AuthService

// auth.service.ts - Constructeur
import { AuditService } from '../audit/audit.service';

constructor(
  private readonly jwtService: JwtService,
  private readonly srpService: SrpService,
  private readonly srpSessionStore: SrpSessionStoreService,
  private readonly auditService: AuditService,  // AJOUTER
  @InjectRepository(User)
  private readonly userRepository: Repository<User>,
) {}

Étape 3 : ImplĂ©mentation complĂšte de loginVerify avec audit

// auth.service.ts - Méthode loginVerify() complÚte
async loginVerify(loginVerifyDto: LoginVerifyDto): Promise<{ accessToken: string; M2: string }> {
  const { email, M1 } = loginVerifyDto;

  // 1. Récupérer session SRP
  const session = await this.srpSessionStore.get(email);
  if (!session) {
    // Audit : tentative sur session inexistante
    await this.auditService.log({
      action: 'AUTH_SRP_VERIFY_FAILURE',
      userId: null,
      metadata: { email, reason: 'SESSION_NOT_FOUND', timestamp: Date.now() },
    });
    throw new UnauthorizedException('Session SRP expirée ou invalide');
  }

  // 2. Vérifier TTL
  if (Date.now() - session.createdAt > this.srpSessionStore.getTtlMs()) {
    await this.srpSessionStore.delete(email);
    // Audit : session expirée
    await this.auditService.log({
      action: 'AUTH_SRP_VERIFY_FAILURE',
      userId: session.userId,
      metadata: { email, reason: 'SESSION_EXPIRED', timestamp: Date.now() },
    });
    throw new UnauthorizedException('Session SRP expirée');
  }

  try {
    // 3. Vérifier preuve client
    const { M2 } = this.srpService.verifyClientProof(
      session.salt, session.verifier, session.A, session.B, session.b, M1,
    );

    // 4. Supprimer session (usage unique)
    await this.srpSessionStore.delete(email);

    // 5. Générer JWT
    const payload = { email, sub: session.userId };
    const accessToken = this.jwtService.sign(payload);

    // 6. Audit : succĂšs authentification
    await this.auditService.log({
      action: 'AUTH_SRP_VERIFY_SUCCESS',
      userId: session.userId,
      metadata: { email, timestamp: Date.now() },
    });

    return { accessToken, M2 };

  } catch (error) {
    // Nettoyage session
    await this.srpSessionStore.delete(email);

    // Audit : échec vérification
    await this.auditService.log({
      action: 'AUTH_SRP_VERIFY_FAILURE',
      userId: session.userId,
      metadata: {
        email,
        reason: error instanceof Error ? error.message : 'UNKNOWN_ERROR',
        timestamp: Date.now(),
      },
    });

    if (error instanceof Error && error.message.includes('Preuve client invalide')) {
      throw new UnauthorizedException('Identifiants invalides');
    }
    throw new InternalServerErrorException("Erreur lors de la vérification");
  }
}

ÉvĂ©nements d'audit dĂ©finis :

Action Contexte Données tracées
AUTH_SRP_VERIFY_SUCCESS Authentification réussie userId, email, timestamp
AUTH_SRP_VERIFY_FAILURE Échec authentification userId (si connu), email, reason, timestamp

Raisons d'échec tracées : - SESSION_NOT_FOUND : Session SRP inexistante - SESSION_EXPIRED : Session SRP expirée (TTL dépassé) - Preuve client invalide : M1 incorrect - UNKNOWN_ERROR : Erreur interne


4. Gestion des erreurs

4.1 Matrice des erreurs

Cas d'erreur (Spec §6) Code HTTP Message Action
Session SRP inexistante 401 Session SRP expirée ou invalide Log audit échec
Session SRP expirée 401 Session SRP expirée Suppression session + log
Preuve M1 invalide 401 Identifiants invalides Suppression session + log
Réutilisation preuve 401 Session SRP expirée ou invalide Session déjà supprimée
Erreur dérivation clé 400 ParamÚtre u invalide (u = 0) Log technique
Erreur génération JWT 500 Erreur lors de la vérification Log technique + alerte

4.2 Code existant

// auth.service.ts:203-244
async loginVerify(...) {
  const session = await this.srpSessionStore.get(email);
  if (!session) {
    throw new UnauthorizedException('Session SRP expirĂ©e ou invalide');  // ✅
  }

  if (Date.now() - session.createdAt > TTL) {
    await this.srpSessionStore.delete(email);
    throw new UnauthorizedException('Session SRP expirĂ©e');  // ✅
  }

  try {
    const { M2 } = this.srpService.verifyClientProof(...);
    await this.srpSessionStore.delete(email);  // ✅ Usage unique
    // ...
  } catch (error) {
    await this.srpSessionStore.delete(email);  // ✅ Nettoyage sur erreur
    if (error.message.includes('Preuve client invalide')) {
      throw new UnauthorizedException('Identifiants invalides');  // ✅
    }
    throw new InternalServerErrorException(...);  // ✅
  }
}

4.3 Réponses non-informatives (sécurité)

Le serveur ne distingue pas publiquement : - Utilisateur inexistant vs preuve invalide → mĂȘme message 401 - Session expirĂ©e vs inexistante → mĂȘme message 401

Cela prévient les attaques par énumération.


5. Impacts sécurité

5.1 Analyse STRIDE

Menace Risque Mitigation
Spoofing Usurpation identité SRP-6a prouve connaissance mot de passe sans le transmettre
Tampering Modification M1/M2 TLS obligatoire, vérification cryptographique
Repudiation Déni d'authentification Audit trail (INV-7)
Information Disclosure Fuite clé session K jamais persistée, scope local (INV-4)
Denial of Service Flood sessions TTL Redis 5 min, rate limiting recommandé
Elevation of Privilege JWT falsifié Signature HMAC/RSA, secret protégé

5.2 Conformité

Norme Exigence Implémentation
RFC 5054 Groupe 3072 bits ✅ SrpService.N
OWASP ASVS Protection mot de passe ✅ Zero-Knowledge
NIST SP 800-63B Stockage sĂ©curisĂ© credentials ✅ Verifier uniquement

5.3 Recommandations complémentaires

  1. Rate limiting : Limiter Ă  5 tentatives/minute par IP
  2. Monitoring : Alertes sur taux d'échec > 10%
  3. Rotation JWT secret : Via Vault/KMS

6. HypothĂšses techniques

# HypothĂšse Impact si fausse
H-1 Redis disponible et opĂ©rationnel Sessions SRP impossibles → auth bloquĂ©e
H-2 Phase 1 (PD-24) correctement implémentée Données session incohérentes
H-3 Horloge serveur synchronisée (NTP) TTL session incohérent
H-4 TLS 1.2+ sur tous les échanges Interception M1/M2 possible
H-5 JWT_SECRET configuré et sécurisé Falsification tokens possible
H-6 Client implémente vérification M2 Auth mutuelle non garantie cÎté client

7. Points de vigilance

7.1 Points critiques

Point Risque Vérification
TTL session Session trop longue = replay attack Test : session expire aprĂšs 5 min
Usage unique session RĂ©utilisation = replay Test : 2Ăšme verify → 401
Suppression sur erreur Session zombie Test : aprÚs échec, session inexistante
Non-persistance K Fuite clé session Audit code : K non stockée

7.2 Réponses aux points à clarifier (Spec §10)

# Point Décision proposée
10.1 Durée validité session SRP 5 minutes (TTL Redis, déjà implémenté)
10.2 Durée de vie JWT 1h (configurable via JWT_EXPIRATION)
10.3 Claims JWT { email, sub (userId), iat, exp }
10.4 Niveau journalisation échecs INFO pour tentatives, WARN pour patterns suspects
10.5 Exigences réglementaires eIDAS niveau substantiel (Zero-Knowledge conforme)

7.3 Tests d'acceptance requis

# ScĂ©nario 1 — Authentification rĂ©ussie
Given une session SRP valide avec A, b, B, salt, verifier
And une preuve client M1 correctement calculée
When POST /auth/login/verify { email, M1 }
Then response.status = 200
And response.body.accessToken est un JWT valide
And response.body.M2 permet au client de vérifier le serveur
And la session SRP est supprimée de Redis

# ScĂ©nario 2 — Preuve invalide
Given une session SRP valide
And une preuve client M1 incorrecte
When POST /auth/login/verify { email, M1 }
Then response.status = 401
And response.body.message = "Identifiants invalides"
And la session SRP est supprimée de Redis
And un événement audit "AUTH_SRP_VERIFY_FAILURE" est émis

# ScĂ©nario 3 — Session expirĂ©e
Given une session SRP créée il y a > 5 minutes
When POST /auth/login/verify { email, M1 }
Then response.status = 401
And response.body.message = "Session SRP expirée"

8. État actuel de l'implĂ©mentation

8.1 ÉlĂ©ments dĂ©jĂ  implĂ©mentĂ©s (PD-24)

ÉlĂ©ment Status Fichier
verifyClientProof() ✅ ImplĂ©mentĂ© srp.service.ts:228-274
loginVerify() endpoint ✅ ImplĂ©mentĂ© auth.controller.ts:152-156
Stockage session Redis ✅ ImplĂ©mentĂ© srp-session-store.service.ts
Suppression session aprĂšs usage ✅ ImplĂ©mentĂ© auth.service.ts:226
GĂ©nĂ©ration JWT ✅ ImplĂ©mentĂ© auth.service.ts:229-230
Retour M2 ✅ ImplĂ©mentĂ© auth.service.ts:232

8.2 ÉlĂ©ments Ă  implĂ©menter (PD-25)

ÉlĂ©ment PrioritĂ© Effort RĂ©fĂ©rence
IntĂ©gration audit (INV-7) Haute 2h §3.1 — Code complet fourni
Tests d'acceptance formels Haute 4h §7.3 — ScĂ©narios Gherkin
Documentation claims JWT Moyenne 1h §7.2 — DĂ©cision 10.3
Rate limiting /auth/login/* Moyenne 2h §5.3 — Recommandation

Note : L'intégration audit (INV-7) est obligatoire pour la conformité spec §4.7. Le code complet est fourni en §3.1.


Fin du plan d'implémentation PD-25.