Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
12 KiB
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 :
{
"email": "user@example.com"
}
Response (200 OK) :
{
"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 :
{
"token": "base64-url-safe-token-here",
"new_password": "NewSecurePassword123!"
}
Response (200 OK) :
{
"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_hashdans tableusers - 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 = TRUEaprè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_hashdans tableusers
🗄️ Modèle de données
Table password_reset_tokens
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_idsuruser_ididx_password_reset_tokens_token_hashsurtoken_hashidx_password_reset_tokens_expires_atsurexpires_at
Règles :
- Un token est valide si :
used = FALSEETexpires_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 DBVerifyToken(token string) (uuid.UUID, error): Vérifie et retourne userIDMarkTokenAsUsed(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 emailValidatePassword(password string) error: Valide la force du mot de passeUpdatePassword(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 resetResetPassword(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
# 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 à jourTestAuthService_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 :
- Créer un utilisateur en DB
- Appeler
/api/v1/auth/password/reset-request - Récupérer le token en DB
- Appeler
/api/v1/auth/password/resetavec le token - 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
# 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 emailWarn: É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éeError: Erreurs de mise à jour mot de passeWarn: É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 :
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(tablepassword_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
- Endpoints fonctionnels (
/reset-requestet/reset) - Tokens stockés en DB avec expiration
- Tokens invalidés après usage
- Prévention énumération emails (réponse uniforme)
- Invalidation sessions après reset
- Validation force mot de passe
- Logs sécurisés (pas de token complet)
- 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