# AUTH_PASSWORD_RESET.md ## 📋 Vue d'ensemble Ce document dĂ©crit le systĂšme complet de rĂ©initialisation de mot de passe (password reset) implĂ©mentĂ© dans `veza-backend-api`. Le systĂšme permet aux utilisateurs de rĂ©initialiser leur mot de passe de maniĂšre sĂ©curisĂ©e via un flux en deux Ă©tapes : demande de reset et confirmation avec token. ## 🎯 Objectifs - Permettre aux utilisateurs de rĂ©initialiser leur mot de passe en cas d'oubli - Garantir la sĂ©curitĂ© via des tokens Ă  usage unique avec expiration - PrĂ©venir l'Ă©numĂ©ration d'emails (email enumeration) - Invalider automatiquement les sessions existantes aprĂšs reset ## 🔄 Flux global ``` 1. User → POST /api/v1/auth/password/reset-request └─> Email fourni └─> Si email existe → GĂ©nĂ©ration token + Stockage DB + Envoi email └─> RĂ©ponse gĂ©nĂ©rique (toujours succĂšs pour sĂ©curitĂ©) 2. User → Email reçu avec lien contenant token └─> Clic sur lien → Frontend avec token en paramĂštre 3. User → POST /api/v1/auth/password/reset └─> Token + Nouveau mot de passe └─> VĂ©rification token (valide, non expirĂ©, non utilisĂ©) └─> Hash nouveau mot de passe └─> Mise Ă  jour password_hash en DB └─> Invalidation token (marquĂ© comme utilisĂ©) └─> Invalidation sessions utilisateur (revoke refresh tokens) ``` ## 📡 Contrat API ### Endpoint 1 : Request Password Reset **Route** : `POST /api/v1/auth/password/reset-request` **Request Body** : ```json { "email": "user@example.com" } ``` **Response (200 OK)** : ```json { "message": "If the email exists, a reset link has been sent" } ``` **Comportement** : - Si l'email existe : gĂ©nĂ©ration token, stockage DB, envoi email - Si l'email n'existe pas : mĂȘme rĂ©ponse (prĂ©vention Ă©numĂ©ration) - Toujours retourne 200 OK avec message gĂ©nĂ©rique **Codes d'erreur** : - `400 Bad Request` : Email invalide (format) - `500 Internal Server Error` : Erreur serveur (gĂ©nĂ©ration token, stockage DB) --- ### Endpoint 2 : Confirm Password Reset **Route** : `POST /api/v1/auth/password/reset` **Request Body** : ```json { "token": "base64-url-safe-token-here", "new_password": "NewSecurePassword123!" } ``` **Response (200 OK)** : ```json { "message": "Password reset successfully" } ``` **Codes d'erreur** : - `400 Bad Request` : - Token invalide ou expirĂ© - Token dĂ©jĂ  utilisĂ© - Mot de passe trop faible (validation) - Format de requĂȘte invalide **Comportement** : - VĂ©rifie token (existe, non expirĂ©, non utilisĂ©) - Valide force du mot de passe - Hash nouveau mot de passe (bcrypt, cost 12) - Met Ă  jour `password_hash` dans table `users` - Marque token comme utilisĂ© - Invalide toutes les sessions utilisateur (revoke refresh tokens) --- ## 🔒 SĂ©curitĂ© ### Tokens - **GĂ©nĂ©ration** : 32 bytes alĂ©atoires, encodĂ©s en base64 URL-safe - **Expiration** : 1 heure (configurable via `PasswordResetService`) - **Usage unique** : Token marquĂ© comme `used = TRUE` aprĂšs utilisation - **Invalidation** : Tous les tokens prĂ©cĂ©dents d'un utilisateur sont invalidĂ©s lors d'une nouvelle demande ### PrĂ©vention d'Ă©numĂ©ration - **RĂ©ponse uniforme** : Toujours retourner le mĂȘme message, mĂȘme si l'email n'existe pas - **Pas de timing attack** : MĂȘme temps de traitement pour email existant/non existant - **Logs sĂ©curisĂ©s** : Jamais logger le token complet, seulement un preview (8 premiers caractĂšres) ### Invalidation des sessions AprĂšs un reset de mot de passe rĂ©ussi : - Tous les refresh tokens de l'utilisateur sont rĂ©voquĂ©s - Les sessions actives sont invalidĂ©es - L'utilisateur doit se reconnecter avec le nouveau mot de passe ### Hash des mots de passe - **Algorithme** : bcrypt - **Cost** : 12 (Ă©quilibre sĂ©curitĂ©/performance) - **Stockage** : Champ `password_hash` dans table `users` --- ## đŸ—„ïž ModĂšle de donnĂ©es ### Table `password_reset_tokens` ```sql CREATE TABLE public.password_reset_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, -- Token token VARCHAR(255) NOT NULL UNIQUE, token_hash VARCHAR(255) NOT NULL, -- Pour future amĂ©lioration -- Status used BOOLEAN NOT NULL DEFAULT false, used_at TIMESTAMPTZ, expires_at TIMESTAMPTZ NOT NULL, -- Metadata ip_address INET, user_agent TEXT, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_password_reset_expires CHECK (expires_at > created_at) ); ``` **Indexes** : - `idx_password_reset_tokens_user_id` sur `user_id` - `idx_password_reset_tokens_token_hash` sur `token_hash` - `idx_password_reset_tokens_expires_at` sur `expires_at` **RĂšgles** : - Un token est valide si : `used = FALSE` ET `expires_at > NOW()` - Sur nouvelle demande, tous les tokens prĂ©cĂ©dents (`used = FALSE`) sont invalidĂ©s --- ## đŸ—ïž Architecture ### Services #### `PasswordResetService` (`internal/services/password_reset_service.go`) MĂ©thodes principales : - `GenerateToken() (string, error)` : GĂ©nĂšre un token alĂ©atoire sĂ©curisĂ© - `StoreToken(userID uuid.UUID, token string) error` : Stocke le token en DB - `VerifyToken(token string) (uuid.UUID, error)` : VĂ©rifie et retourne userID - `MarkTokenAsUsed(token string) error` : Marque le token comme utilisĂ© - `InvalidateOldTokens(userID uuid.UUID) error` : Invalide tous les tokens prĂ©cĂ©dents #### `PasswordService` (`internal/services/password_service.go`) MĂ©thodes utilisĂ©es : - `GetUserByEmail(email string) (*UserInfo, error)` : RĂ©cupĂšre utilisateur par email - `ValidatePassword(password string) error` : Valide la force du mot de passe - `UpdatePassword(userID uuid.UUID, newPassword string) error` : Met Ă  jour le mot de passe #### `EmailService` (`internal/services/email_service.go`) MĂ©thodes utilisĂ©es : - `SendPasswordResetEmail(userID uuid.UUID, email string, token string) error` : Envoie l'email de reset #### `AuthService` (`internal/core/auth/service.go`) MĂ©thodes principales : - `RequestPasswordReset(ctx context.Context, email string) error` : Orchestre la demande de reset - `ResetPassword(ctx context.Context, token string, newPassword string) error` : Orchestre la confirmation de reset ### Handlers #### `RequestPasswordReset` (`internal/handlers/password_reset_handler.go`) Handler HTTP pour la demande de reset : - Valide l'email - Trouve l'utilisateur (ou retourne succĂšs gĂ©nĂ©rique) - GĂ©nĂšre et stocke le token - Envoie l'email - Retourne rĂ©ponse gĂ©nĂ©rique #### `ResetPassword` (`internal/handlers/password_reset_handler.go`) Handler HTTP pour la confirmation de reset : - Valide le token - Valide le nouveau mot de passe - Met Ă  jour le mot de passe - Marque le token comme utilisĂ© - Invalide les sessions utilisateur --- ## ⚙ Configuration ### Variables d'environnement ```bash # URL du frontend (pour construire le lien de reset) FRONTEND_URL=http://localhost:5173 # DĂ©faut si non dĂ©fini # Configuration SMTP (pour envoi emails) SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_USER=your-email@example.com SMTP_PASSWORD=your-password FROM_EMAIL=noreply@veza.com FROM_NAME=Veza ``` ### Configuration du service Le `PasswordResetService` utilise une expiration de **1 heure** par dĂ©faut (non configurable actuellement, hardcodĂ© dans `StoreToken`). --- ## đŸ§Ș Tests ### Tests unitaires **Fichier** : `internal/core/auth/service_test.go` (Ă  crĂ©er) Tests Ă  implĂ©menter : - `TestAuthService_RequestPasswordReset_UserExists` : Token gĂ©nĂ©rĂ© et stockĂ© - `TestAuthService_RequestPasswordReset_UserNotExists` : Retourne nil (pas d'erreur) - `TestAuthService_ResetPassword_ValidToken` : Mot de passe mis Ă  jour - `TestAuthService_ResetPassword_ExpiredToken` : Erreur "token expired" - `TestAuthService_ResetPassword_UsedToken` : Erreur "token already used" - `TestAuthService_ResetPassword_InvalidToken` : Erreur "invalid token" ### Tests d'intĂ©gration **Fichier** : `tests/integration/password_reset_test.go` (Ă  crĂ©er) Test complet du flux : 1. CrĂ©er un utilisateur en DB 2. Appeler `/api/v1/auth/password/reset-request` 3. RĂ©cupĂ©rer le token en DB 4. Appeler `/api/v1/auth/password/reset` avec le token 5. VĂ©rifier que le nouveau mot de passe permet un login **Note** : Peut ĂȘtre marquĂ© comme `t.Skip` si l'infra de test n'est pas configurĂ©e. ### Lancer les tests ```bash # Tests unitaires du service auth cd veza-backend-api go test ./internal/core/auth -run TestAuthService.*PasswordReset -v # Tests d'intĂ©gration (si configurĂ©s) go test ./tests/integration -run TestPasswordReset -v ``` --- ## 📝 Logs ### Ce qui est loggĂ© - **RequestPasswordReset** : - `Info` : "Password reset requested successfully" (avec email, user_id, token preview) - `Error` : Erreurs de gĂ©nĂ©ration token, stockage, envoi email - `Warn` : Échec invalidation anciens tokens (non bloquant) - **ResetPassword** : - `Info` : "Password reset completed successfully" (avec user_id) - `Warn` : Token invalide/expirĂ©/utilisĂ©, validation mot de passe Ă©chouĂ©e - `Error` : Erreurs de mise Ă  jour mot de passe - `Warn` : Échec marquage token comme utilisĂ© (non bloquant) - `Warn` : Échec invalidation sessions (non bloquant) ### Ce qui n'est JAMAIS loggĂ© - **Token complet** : Seulement un preview (8 premiers caractĂšres + "...") - **Nouveau mot de passe** : Jamais loggĂ©, mĂȘme hashĂ© - **Email utilisateur** : LoggĂ© uniquement pour debugging (peut ĂȘtre masquĂ© en production) --- ## 🔧 Maintenance ### Nettoyage des tokens expirĂ©s Les tokens expirĂ©s peuvent ĂȘtre nettoyĂ©s pĂ©riodiquement via un job de maintenance : ```sql DELETE FROM password_reset_tokens WHERE expires_at < NOW() - INTERVAL '7 days' AND used = TRUE; ``` **Note** : Un job de cleanup n'est pas encore implĂ©mentĂ©, mais peut ĂȘtre ajoutĂ© dans `internal/jobs/`. ### Monitoring MĂ©triques Ă  surveiller : - Nombre de demandes de reset par jour - Taux d'Ă©chec de vĂ©rification token (tokens expirĂ©s/invalides) - Taux de succĂšs de reset (token utilisĂ© avec succĂšs) - Temps moyen entre demande et confirmation --- ## 🐛 DĂ©pannage ### ProblĂšme : Token invalide ou expirĂ© **Causes possibles** : - Token dĂ©jĂ  utilisĂ© - Token expirĂ© (> 1h) - Token incorrect (copie/collage partiel) **Solution** : Demander un nouveau token via `/api/v1/auth/password/reset-request` ### ProblĂšme : Email non reçu **Causes possibles** : - Configuration SMTP incorrecte - Email dans spam - Email invalide **VĂ©rifications** : - Logs serveur pour erreurs SMTP - VĂ©rifier `SMTP_*` variables d'environnement - VĂ©rifier que l'utilisateur existe en DB ### ProblĂšme : Sessions non invalidĂ©es aprĂšs reset **Cause** : Échec de `refreshTokenService.RevokeAll()` **Solution** : VĂ©rifier les logs, le mot de passe est dĂ©jĂ  mis Ă  jour (non bloquant) --- ## 📚 RĂ©fĂ©rences - Migration : `migrations/010_auth_and_users.sql` (table `password_reset_tokens`) - Service : `internal/services/password_reset_service.go` - Handler : `internal/handlers/password_reset_handler.go` - Auth Service : `internal/core/auth/service.go` - Router : `internal/api/router.go` (routes `/api/v1/auth/password/*`) --- ## ✅ Checklist de validation - [x] Endpoints fonctionnels (`/reset-request` et `/reset`) - [x] Tokens stockĂ©s en DB avec expiration - [x] Tokens invalidĂ©s aprĂšs usage - [x] PrĂ©vention Ă©numĂ©ration emails (rĂ©ponse uniforme) - [x] Invalidation sessions aprĂšs reset - [x] Validation force mot de passe - [x] Logs sĂ©curisĂ©s (pas de token complet) - [x] Documentation complĂšte - [ ] Tests unitaires complets (Ă  complĂ©ter) - [ ] Test d'intĂ©gration (Ă  complĂ©ter si infra disponible) --- **DerniĂšre mise Ă  jour** : 2025-01-XX **Version** : 1.0.0 **Auteur** : Équipe Veza Backend