đ ïž 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 :
src/modules/auth/auth.module.tsâ ImporterAuditModulesrc/modules/auth/auth.service.tsâ Injecter et appelerAuditService
Ă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¶
- Rate limiting : Limiter Ă 5 tentatives/minute par IP
- Monitoring : Alertes sur taux d'échec > 10%
- 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.