# 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

