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).
16 KiB
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
-
1. Backend — Modèle de données et persistance
-
1.1 Créer les entités
CompteetResetPasswordToken- Créer
Models/Entities/Compte.cs(singletonId = 1non 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
- Créer
-
1.2 Déclarer les DbSet et la configuration
OnModelCreating- Ajouter
DbSet<Compte>etDbSet<ResetPasswordToken>dansData/AppDbContext.cs - Configurer
Compte:HasKey,IdValueGeneratedNever(), index uniqueUX_Comptes_EmailsurEmail - Configurer
ResetPasswordToken: index uniqueUX_ResetTokens_TokenHash, indexIX_ResetTokens_Expiration - Requirements: 1.1, 1.2, 6.1
- Ajouter
-
1.3 Générer la migration EF
AjoutAuthentification- Exécuter
dotnet ef migrations add AjoutAuthentificationpour créer les tablesComptesetResetPasswordTokensavec leurs index - Vérifier que la migration est appliquée par le
MigrateAsync()existant deProgram.cs(aucune modification du démarrage requise) - Requirements: 1.1, 4.4
- Exécuter
-
-
2. Backend — Options de configuration
-
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(...))dansProgram.cs - Requirements: 5.5, 6.3, 6.7
- Créer
-
* 2.2 Écrire un test de liaison des options
- Vérifier que
JwtOptions,AuthOptions,SmtpOptionsse lient correctement depuis la configuration (démarrage) - Requirements: 5.5, 6.3, 6.7
- Vérifier que
-
-
3. Backend — Hachage des mots de passe
-
3.1 Intégrer
PasswordHasher<Compte>en DI- Enregistrer
IPasswordHasher<Compte>->PasswordHasher<Compte>en singleton dansProgram.cs - Requirements: 1.5, 8.1, 8.3
- Enregistrer
-
* 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
-
-
4. Backend — Service JWT
-
4.1 Implémenter
IJwtService/JwtService- Créer
Services/IJwtService.csetServices/JwtService.cs GenererJeton(Compte): émission HS256 avec claimssub,email,jti, expiration = maintenant +JwtOptions.ExpirationMinutes; aucune donnée sensible (jamais le hash)- Enregistrer
IJwtService->JwtService(scoped) dansProgram.cs - Requirements: 2.1, 2.4, 5.1, 5.4, 5.5
- Créer
-
* 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
-
* 4.3 Écrire le test de propriété jeton expiré
- Property 3 : Un jeton JWT expiré est rejeté
- Validates: Requirements 4.3, 5.2
-
* 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
-
-
5. Backend — Service email
-
5.1 Implémenter
IEmailServiceet ses deux implémentations- Créer
Services/IEmailService.cs(EnvoyerLienResetAsync(destinataire, lienReset, ct)) - Créer
Services/SmtpEmailService.cs(MailKit, configuré parSmtpOptions, email HTML/texte contenant le lien) - Créer
Services/LoggingEmailService.cs(repli : journalise le lien viaILoggerniveau Information) - Ajouter la dépendance NuGet MailKit au projet API
- Requirements: 6.4, 6.5, 6.7
- Créer
-
5.2 Câbler la sélection DI selon
Smtp__Host- Dans
Program.cs, enregistrerLoggingEmailServicesiSmtp:Hostest vide, sinonSmtpEmailService - Requirements: 6.4, 6.5
- Dans
-
* 5.3 Écrire le test de sélection DI de
IEmailServiceSmtp__Hostvide ⇒LoggingEmailService; renseigné ⇒SmtpEmailService- Requirements: 6.4, 6.5
-
-
6. Backend — DTOs, validateurs et service d'authentification
-
6.1 Créer les DTOs d'authentification et les validateurs FluentValidation
- Créer
Models/DTOs/AuthDtos.cs(recordsRegisterDto,LoginDto,LoginResultDto,ForgotPasswordDto,ResetPasswordDto,CompteExisteDto) — aucun n'exposeMotDePasseHash - Créer les validateurs FluentValidation : email syntaxiquement valide,
MotDePasse/NouveauMotDePasse>= 8 caractères - Requirements: 1.3, 1.4, 7.5, 8.2
- Créer
-
6.2 Implémenter
IAuthService/AuthService- Créer
Services/IAuthService.csetServices/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 viaVerifyHashedPassword, retourne JWT, sinon exception générique -> 401)ForgotPasswordAsync(réponse générique ; si email connu : crée unResetPasswordTokenhaché avec expiration = now +ResetTokenLifetimeMinutes, construit le lien{FrontendBaseUrl}/reset-password?token=..., appelleIEmailService) ;ResetPasswordAsync(jeton valide/non expiré/non utilisé, remplace le mdp haché, marqueUsedAt)- Enregistrer
IAuthService->AuthService(scoped) dansProgram.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
- Créer
-
* 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
-
* 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
-
* 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
-
* 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
-
* 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
-
* 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
-
* 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
-
* 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
-
* 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
-
* 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
-
* 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
- Emails invalides (
-
-
7. Backend — Controller d'authentification
- 7.1 Implémenter
AuthControlleravec 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
- Créer
- 7.1 Implémenter
-
8. Backend — Sécurité du pipeline
-
8.1 Configurer l'authentification JWT et l'autorisation globale
- Ajouter
AddAuthentication(JwtBearer).AddJwtBearer(...)avecTokenValidationParameters(issuer, audience, lifetime, signing key depuisJwtOptions,ClockSkew30s) - Ajouter le filtre global
AuthorizeFilter(RequireAuthenticatedUser())surAddControllers - Insérer
app.UseAuthentication()avantapp.UseAuthorization() - Conserver
[AllowAnonymous]surAuthControlleret laisser/healthpublic - Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 5.1, 5.2, 5.3
- Ajouter
-
* 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/healthaccessibles sans jeton- Requirements: 4.1, 4.2, 4.4, 4.5
- Via
-
* 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
-
-
9. Checkpoint backend
- Ensure all tests pass, ask the user if questions arise.
-
10. Frontend — Service, modèles, interceptor et garde
-
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_tokeninitialisé depuislocalStorage,estAuthentifie(computed),login/register/forgotPassword/resetPassword/compteExiste/logout/getToken, base${environment.apiUrl}/auth - Requirements: 2.3, 3.1, 3.2, 9.1
- Créer les types
-
10.2 Implémenter
authInterceptoret l'enregistrer avanttimeoutInterceptor- Créer
core/interceptors/auth.interceptor.ts: ajouteAuthorization: Beareraux requêtes versenvironment.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
- Créer
-
10.3 Implémenter
authGuard- Créer
core/guards/auth.guard.ts(CanActivateFn) : si non authentifié ->UrlTreevers/login, sinontrue - Requirements: 4.6, 9.3
- Créer
-
* 10.4 Écrire les tests de service, interceptor et garde
AuthServicestocke/supprime le token danslocalStorage;authGuardredirige un non-authentifié vers/login;authInterceptorpurge le token et redirige sur 401- Requirements: 2.3, 3.1, 4.6, 4.7, 9.3
-
-
11. Frontend — Écrans standalone (signals)
-
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
-
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
- Formulaire email + mot de passe ; sur succès redirige vers
-
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
-
11.4 Implémenter
ResetPasswordComponent(/reset-password)- Lit le
tokendepuis 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
- Lit le
-
* 11.5 Écrire le test de lecture du token par
ResetPasswordComponent- Vérifie la lecture du
tokendepuis l'URL - Requirements: 7.6
- Vérifie la lecture du
-
-
12. Frontend — Routes et onboarding en deux étapes
- 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+/vehiculespar[authGuard] - Conserver l'
OnboardingComponentexistant comme écran de l'Etape 2 (comportement inchangé) - Requirements: 4.6, 9.3, 9.4, 9.5, 9.6
- Ajouter les routes publiques
- 12.1 Mettre à jour
-
13. Configuration et déploiement
-
13.1 Mettre à jour
.env.exampleetdocker-compose.yml.env.example: documenterJWT_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(servicebackend) : mapperJWT__SecretKey(:?requis),JWT__ExpirationMinutes,Auth__ResetTokenLifetimeMinutes,Auth__FrontendBaseUrl,Smtp__*- Requirements: 5.5, 6.3, 6.7
-
13.2 Vérifier les builds
- Exécuter
dotnet buildsur le backend et corriger les erreurs de compilation - Exécuter
ng buildsur le frontendschotziad-km-trackeret corriger les erreurs de compilation - Requirements: 4.4, 9.6
- Exécuter
-
-
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
UseAuthenticationavantUseAuthorization), puis frontend zoneless (signals) et déploiement Docker.
Task Dependency Graph
{
"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"] }
]
}