Ajout de l'authentification (compte unique, JWT, reset par email)

Application mono-utilisateur : inscription unique, connexion par JWT (12 h),
protection de toutes les routes de données, réinitialisation de mot de passe
par email avec repli de journalisation, onboarding réorganisé en deux étapes
(inscription → connexion → profil + véhicule).

Backend (SchotzIadKmTracker.Api) :
- Entités Compte (singleton) et ResetPasswordToken + migration AjoutAuthentification.
- Hachage PasswordHasher, JwtService (HS256, durée configurable), IEmailService
  (MailKit + repli LoggingEmailService selon Smtp:Host).
- AuthService (register 409 si compte existant, login 401 générique, forgot-password
  non-divulgation, reset à usage unique + expiration), AuthController (5 endpoints
  publics), DTOs + validateurs FluentValidation.
- Sécurité pipeline : JwtBearer + filtre d'autorisation global, UseAuthentication
  avant UseAuthorization ; /health et /api/auth/* publics.

Frontend (schotziad-km-tracker, zoneless + signals) :
- AuthService (localStorage), authInterceptor (Bearer + purge/redirection 401),
  authGuard, écrans Login/Register/ForgotPassword/ResetPassword, routes publiques
  et protection [authGuard, profilConfigureGuard].

Configuration : .env.example et docker-compose.yml (JWT__SecretKey, durées, Smtp__*).

Tests : 54 tests backend (propriétés FsCheck P1–P14, exemples, intégration
WebApplicationFactory) + tests frontend (AuthService, authGuard, authInterceptor,
ResetPasswordComponent). Suite complète au vert.

Spec : .kiro/specs/authentification (requirements, design, tasks).
This commit is contained in:
2026-09-14 09:07:08 +02:00
parent 0e6a97ca61
commit dd36bbc425
66 changed files with 6282 additions and 27 deletions
+250
View File
@@ -0,0 +1,250 @@
# Implementation Plan : Authentification
## Overview
Ce plan découpe l'ajout de l'authentification en tâches de codage incrémentales, chacune s'appuyant sur les précédentes et se terminant par un câblage complet (pas de code orphelin). Le backend (`SchotzIadKmTracker.Api`, ASP.NET Core .NET 8, EF Core + SQLite) est construit couche par couche — modèle → options → hachage → JWT → email → service métier → controller → sécurité pipeline — puis le frontend Angular zoneless (signals) — service → interceptor/garde → écrans → routes — et enfin la configuration/déploiement. Les tests de propriétés (FsCheck.Xunit) et d'exemples/intégration (xUnit + WebApplicationFactory) sont des sous-tâches placées au plus près du code qu'elles valident.
## Tasks
- [x] 1. Backend — Modèle de données et persistance
- [x] 1.1 Créer les entités `Compte` et `ResetPasswordToken`
- Créer `Models/Entities/Compte.cs` (singleton `Id = 1` non généré, `Email`, `MotDePasseHash`, `CreatedAt`, `UpdatedAt`)
- Créer `Models/Entities/ResetPasswordToken.cs` (`Id`, `TokenHash`, `Expiration`, `UsedAt?`, `CreatedAt`)
- _Requirements: 1.5, 6.1, 7.2, 8.1_
- [x] 1.2 Déclarer les DbSet et la configuration `OnModelCreating`
- Ajouter `DbSet<Compte>` et `DbSet<ResetPasswordToken>` dans `Data/AppDbContext.cs`
- Configurer `Compte` : `HasKey`, `Id` `ValueGeneratedNever()`, index unique `UX_Comptes_Email` sur `Email`
- Configurer `ResetPasswordToken` : index unique `UX_ResetTokens_TokenHash`, index `IX_ResetTokens_Expiration`
- _Requirements: 1.1, 1.2, 6.1_
- [x] 1.3 Générer la migration EF `AjoutAuthentification`
- Exécuter `dotnet ef migrations add AjoutAuthentification` pour créer les tables `Comptes` et `ResetPasswordTokens` avec leurs index
- Vérifier que la migration est appliquée par le `MigrateAsync()` existant de `Program.cs` (aucune modification du démarrage requise)
- _Requirements: 1.1, 4.4_
- [x] 2. Backend — Options de configuration
- [x] 2.1 Créer les classes d'options et les lier à la configuration
- Créer `Configuration/JwtOptions.cs` (`SecretKey`, `Issuer`, `Audience`, `ExpirationMinutes = 720`, `SectionName = "Jwt"`)
- Créer `Configuration/AuthOptions.cs` (`ResetTokenLifetimeMinutes = 60`, `FrontendBaseUrl`, `SectionName = "Auth"`)
- Créer `Configuration/SmtpOptions.cs` (`Host`, `Port = 587`, `User`, `Password`, `From`, `SectionName = "Smtp"`)
- Enregistrer les trois via `builder.Services.Configure<...>(...GetSection(...))` dans `Program.cs`
- _Requirements: 5.5, 6.3, 6.7_
- [x]* 2.2 Écrire un test de liaison des options
- Vérifier que `JwtOptions`, `AuthOptions`, `SmtpOptions` se lient correctement depuis la configuration (démarrage)
- _Requirements: 5.5, 6.3, 6.7_
- [x] 3. Backend — Hachage des mots de passe
- [x] 3.1 Intégrer `PasswordHasher<Compte>` en DI
- Enregistrer `IPasswordHasher<Compte>` -> `PasswordHasher<Compte>` en singleton dans `Program.cs`
- _Requirements: 1.5, 8.1, 8.3_
- [x]* 3.2 Écrire le test de propriété du hachage
- **Property 1 : Round-trip du hachage de mot de passe**
- **Validates: Requirements 1.5, 8.1, 8.3**
- [x] 4. Backend — Service JWT
- [x] 4.1 Implémenter `IJwtService` / `JwtService`
- Créer `Services/IJwtService.cs` et `Services/JwtService.cs`
- `GenererJeton(Compte)` : émission HS256 avec claims `sub`, `email`, `jti`, expiration = maintenant + `JwtOptions.ExpirationMinutes` ; aucune donnée sensible (jamais le hash)
- Enregistrer `IJwtService` -> `JwtService` (scoped) dans `Program.cs`
- _Requirements: 2.1, 2.4, 5.1, 5.4, 5.5_
- [x]* 4.2 Écrire le test de propriété émission/validation et claims
- **Property 2 : Émission/validation JWT et préservation des claims**
- **Validates: Requirements 2.1, 2.4, 5.1, 5.4, 5.5**
- [x]* 4.3 Écrire le test de propriété jeton expiré
- **Property 3 : Un jeton JWT expiré est rejeté**
- **Validates: Requirements 4.3, 5.2**
- [x]* 4.4 Écrire le test de propriété signature invalide
- **Property 4 : Une signature JWT invalide est rejetée**
- **Validates: Requirements 5.3, 4.2**
- [x] 5. Backend — Service email
- [x] 5.1 Implémenter `IEmailService` et ses deux implémentations
- Créer `Services/IEmailService.cs` (`EnvoyerLienResetAsync(destinataire, lienReset, ct)`)
- Créer `Services/SmtpEmailService.cs` (MailKit, configuré par `SmtpOptions`, email HTML/texte contenant le lien)
- Créer `Services/LoggingEmailService.cs` (repli : journalise le lien via `ILogger` niveau Information)
- Ajouter la dépendance NuGet MailKit au projet API
- _Requirements: 6.4, 6.5, 6.7_
- [x] 5.2 Câbler la sélection DI selon `Smtp__Host`
- Dans `Program.cs`, enregistrer `LoggingEmailService` si `Smtp:Host` est vide, sinon `SmtpEmailService`
- _Requirements: 6.4, 6.5_
- [x]* 5.3 Écrire le test de sélection DI de `IEmailService`
- `Smtp__Host` vide ⇒ `LoggingEmailService` ; renseigné ⇒ `SmtpEmailService`
- _Requirements: 6.4, 6.5_
- [x] 6. Backend — DTOs, validateurs et service d'authentification
- [x] 6.1 Créer les DTOs d'authentification et les validateurs FluentValidation
- Créer `Models/DTOs/AuthDtos.cs` (records `RegisterDto`, `LoginDto`, `LoginResultDto`, `ForgotPasswordDto`, `ResetPasswordDto`, `CompteExisteDto`) — aucun n'expose `MotDePasseHash`
- Créer les validateurs FluentValidation : email syntaxiquement valide, `MotDePasse`/`NouveauMotDePasse` >= 8 caractères
- _Requirements: 1.3, 1.4, 7.5, 8.2_
- [x] 6.2 Implémenter `IAuthService` / `AuthService`
- Créer `Services/IAuthService.cs` et `Services/AuthService.cs` + exceptions métier (compte existant, identifiants invalides, lien invalide/expiré)
- `CompteExisteAsync` ; `RegisterAsync` (compte unique, sinon exception -> 409, hachage du mdp) ; `LoginAsync` (vérifie via `VerifyHashedPassword`, retourne JWT, sinon exception générique -> 401)
- `ForgotPasswordAsync` (réponse générique ; si email connu : crée un `ResetPasswordToken` haché avec expiration = now + `ResetTokenLifetimeMinutes`, construit le lien `{FrontendBaseUrl}/reset-password?token=...`, appelle `IEmailService`) ; `ResetPasswordAsync` (jeton valide/non expiré/non utilisé, remplace le mdp haché, marque `UsedAt`)
- Enregistrer `IAuthService` -> `AuthService` (scoped) dans `Program.cs`
- _Requirements: 1.1, 1.2, 2.1, 2.2, 6.1, 6.2, 6.4, 6.5, 6.6, 7.1, 7.2, 7.3, 7.4, 8.3, 9.1_
- [x]* 6.3 Écrire le test de propriété inscription crée le compte
- **Property 5 : L'inscription crée le compte lorsqu'aucun compte n'existe**
- **Validates: Requirements 1.1**
- [x]* 6.4 Écrire le test de propriété inscription rejetée si compte existant
- **Property 6 : L'inscription est rejetée lorsqu'un compte existe déjà**
- **Validates: Requirements 1.2**
- [x]* 6.5 Écrire le test de propriété mot de passe trop court à l'inscription
- **Property 7 : Un mot de passe trop court est refusé à l'inscription**
- **Validates: Requirements 1.3**
- [x]* 6.6 Écrire le test de propriété connexion (émission/rejet)
- **Property 8 : La connexion émet un jeton sur identifiants corrects et rejette les identifiants invalides**
- **Validates: Requirements 2.1, 2.2**
- [x]* 6.7 Écrire le test de propriété non-divulgation forgot-password
- **Property 9 : Non-divulgation lors de la demande de réinitialisation**
- **Validates: Requirements 6.6**
- [x]* 6.8 Écrire le test de propriété expiration du jeton de reset
- **Property 10 : Expiration du jeton de reset paramétrée**
- **Validates: Requirements 6.1, 6.2, 6.3**
- [x]* 6.9 Écrire le test de propriété réinitialisation valide remplace le mot de passe
- **Property 11 : Une réinitialisation valide remplace le mot de passe**
- **Validates: Requirements 7.1**
- [x]* 6.10 Écrire le test de propriété jeton de reset à usage unique
- **Property 12 : Un jeton de reset est à usage unique**
- **Validates: Requirements 7.2, 7.4**
- [x]* 6.11 Écrire le test de propriété jeton de reset expiré ou inconnu
- **Property 13 : Un jeton de reset expiré ou inconnu est rejeté**
- **Validates: Requirements 7.3, 7.4**
- [x]* 6.12 Écrire le test de propriété nouveau mot de passe trop court à la réinitialisation
- **Property 14 : Un nouveau mot de passe trop court est refusé à la réinitialisation**
- **Validates: Requirements 7.5**
- [x]* 6.13 Écrire les tests d'exemples de validation et d'exclusion du hash
- Emails invalides (`"a@"`, `"abc"`, `"@x"`, chaîne vide) ⇒ 400 ; email valide accepté
- Les corps de réponse d'auth ne contiennent aucun champ de hash
- _Requirements: 1.4, 8.2_
- [x] 7. Backend — Controller d'authentification
- [x] 7.1 Implémenter `AuthController` avec les 5 endpoints
- Créer `Controllers/AuthController.cs` (`[ApiController]`, `[Route("api/auth")]`, `[AllowAnonymous]`)
- `POST /register`, `POST /login`, `POST /forgot-password`, `POST /reset-password`, `GET /compte-existe`
- Mapper les exceptions métier en codes HTTP via `ProblemDetails` : 409 (compte existant), 401 (identifiants génériques), 400 (mdp court / email invalide / lien invalide ou expiré), 200 générique (forgot-password)
- _Requirements: 1.2, 1.3, 1.4, 2.1, 2.2, 6.6, 7.3, 7.4, 7.5, 9.1_
- [x] 8. Backend — Sécurité du pipeline
- [x] 8.1 Configurer l'authentification JWT et l'autorisation globale
- Ajouter `AddAuthentication(JwtBearer).AddJwtBearer(...)` avec `TokenValidationParameters` (issuer, audience, lifetime, signing key depuis `JwtOptions`, `ClockSkew` 30s)
- Ajouter le filtre global `AuthorizeFilter(RequireAuthenticatedUser())` sur `AddControllers`
- Insérer `app.UseAuthentication()` **avant** `app.UseAuthorization()`
- Conserver `[AllowAnonymous]` sur `AuthController` et laisser `/health` public
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 5.1, 5.2, 5.3_
- [x]* 8.2 Écrire le test d'intégration de protection des routes
- Via `WebApplicationFactory<Program>` : routes de données (`/api/trajets`, `/api/profil`, `/api/vehicules`, `/api/bareme`, `/api/resumes`, exports PDF, recalcul) ⇒ 401 sans jeton, 2xx avec jeton valide
- `/api/auth/*` et `/health` accessibles sans jeton
- _Requirements: 4.1, 4.2, 4.4, 4.5_
- [x]* 8.3 Écrire le test d'intégration bout en bout
- register → login → appel protégé avec le token ; puis forgot → reset → login avec le nouveau mot de passe
- _Requirements: 1.1, 2.1, 7.1_
- [x] 9. Checkpoint backend
- Ensure all tests pass, ask the user if questions arise.
- [x] 10. Frontend — Service, modèles, interceptor et garde
- [x] 10.1 Créer les modèles TS et `AuthService` (signals + localStorage)
- Créer les types `LoginDto`, `RegisterDto`, `LoginResult`, `ResetPasswordDto`
- Créer `core/services/auth.service.ts` : signal `_token` initialisé depuis `localStorage`, `estAuthentifie` (computed), `login/register/forgotPassword/resetPassword/compteExiste/logout/getToken`, base `${environment.apiUrl}/auth`
- _Requirements: 2.3, 3.1, 3.2, 9.1_
- [x] 10.2 Implémenter `authInterceptor` et l'enregistrer avant `timeoutInterceptor`
- Créer `core/interceptors/auth.interceptor.ts` : ajoute `Authorization: Bearer` aux requêtes vers `environment.apiUrl` (hors endpoints publics d'auth et appels externes) ; sur 401, purge le token et redirige vers `/login`
- Dans `app.config.ts` : `withInterceptors([authInterceptor, timeoutInterceptor])`
- _Requirements: 4.7_
- [x] 10.3 Implémenter `authGuard`
- Créer `core/guards/auth.guard.ts` (`CanActivateFn`) : si non authentifié -> `UrlTree` vers `/login`, sinon `true`
- _Requirements: 4.6, 9.3_
- [x]* 10.4 Écrire les tests de service, interceptor et garde
- `AuthService` stocke/supprime le token dans `localStorage` ; `authGuard` redirige un non-authentifié vers `/login` ; `authInterceptor` purge le token et redirige sur 401
- _Requirements: 2.3, 3.1, 4.6, 4.7, 9.3_
- [x] 11. Frontend — Écrans standalone (signals)
- [x] 11.1 Implémenter `LoginComponent` (`/login`)
- Formulaire email + mot de passe ; sur succès, stocke le token puis navigue vers la cible protégée ; état async via signals
- _Requirements: 2.1, 2.3_
- [x] 11.2 Implémenter `RegisterComponent` (`/register`, Etape 1)
- Formulaire email + mot de passe ; sur succès redirige vers `/login` ; état async via signals
- _Requirements: 1.6, 9.1, 9.2_
- [x] 11.3 Implémenter `ForgotPasswordComponent` (`/forgot-password`)
- Saisie email ; affiche un message générique quel que soit le résultat ; état async via signals
- _Requirements: 6.6_
- [x] 11.4 Implémenter `ResetPasswordComponent` (`/reset-password`)
- Lit le `token` depuis les query params de l'URL, saisit le nouveau mot de passe ; sur succès redirige vers `/login` ; état async via signals
- _Requirements: 7.6_
- [x]* 11.5 Écrire le test de lecture du token par `ResetPasswordComponent`
- Vérifie la lecture du `token` depuis l'URL
- _Requirements: 7.6_
- [x] 12. Frontend — Routes et onboarding en deux étapes
- [x] 12.1 Mettre à jour `app.routes.ts`
- Ajouter les routes publiques `/login`, `/register`, `/forgot-password`, `/reset-password` (sans garde)
- Protéger les routes de données par `[authGuard, profilConfigureGuard]` et `/onboarding` (Etape 2) + `/profil` + `/vehicules` par `[authGuard]`
- Conserver l'`OnboardingComponent` existant comme écran de l'Etape 2 (comportement inchangé)
- _Requirements: 4.6, 9.3, 9.4, 9.5, 9.6_
- [x] 13. Configuration et déploiement
- [x] 13.1 Mettre à jour `.env.example` et `docker-compose.yml`
- `.env.example` : documenter `JWT_SECRET_KEY`, `JWT_EXPIRATION_MINUTES=720`, `AUTH_RESET_TOKEN_LIFETIME_MINUTES=60`, `AUTH_FRONTEND_BASE_URL`, `SMTP_HOST/PORT/USER/PASSWORD/FROM` (sans valeurs secrètes réelles)
- `docker-compose.yml` (service `backend`) : mapper `JWT__SecretKey` (`:?` requis), `JWT__ExpirationMinutes`, `Auth__ResetTokenLifetimeMinutes`, `Auth__FrontendBaseUrl`, `Smtp__*`
- _Requirements: 5.5, 6.3, 6.7_
- [x] 13.2 Vérifier les builds
- Exécuter `dotnet build` sur le backend et corriger les erreurs de compilation
- Exécuter `ng build` sur le frontend `schotziad-km-tracker` et corriger les erreurs de compilation
- _Requirements: 4.4, 9.6_
- [x] 14. Checkpoint final
- Ensure all tests pass, ask the user if questions arise.
## Notes
- Les tâches marquées `*` sont des tests optionnels et peuvent être ignorées pour un MVP plus rapide ; elles ne sont pas implémentées automatiquement.
- Chaque tâche référence des exigences précises pour la traçabilité, et chaque test de propriété référence sa propriété de conception (P1–P14).
- Les tests de propriétés (FsCheck.Xunit, ≥ 100 itérations) valident les propriétés universelles ; les tests d'exemples et d'intégration (xUnit + WebApplicationFactory) couvrent la validation d'entrée, la sélection DI et la protection des routes.
- Les checkpoints assurent une validation incrémentale (backend d'abord, puis l'ensemble).
- Le séquençage respecte le contexte projet : entités → migration EF appliquée au démarrage → options → services → controller → sécurité pipeline (ordre `UseAuthentication` avant `UseAuthorization`), puis frontend zoneless (signals) et déploiement Docker.
## Task Dependency Graph
```json
{
"waves": [
{ "id": 0, "tasks": ["1.1", "2.1", "10.1"] },
{ "id": 1, "tasks": ["1.2", "2.2", "3.1", "5.1", "10.2", "10.3", "11.1", "11.2", "11.3", "11.4"] },
{ "id": 2, "tasks": ["1.3", "3.2", "4.1", "5.2", "6.1", "10.4", "11.5", "12.1"] },
{ "id": 3, "tasks": ["4.2", "4.3", "4.4", "5.3", "6.2", "13.1"] },
{ "id": 4, "tasks": ["6.3", "6.4", "6.5", "6.6", "6.7", "6.8", "6.9", "6.10", "6.11", "6.12", "6.13", "7.1"] },
{ "id": 5, "tasks": ["8.1"] },
{ "id": 6, "tasks": ["8.2", "8.3", "13.2"] }
]
}
```