395 lines
12 KiB
Markdown
395 lines
12 KiB
Markdown
|
|
# 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
|
||
|
|
|