package middleware import ( "context" "crypto/rand" "crypto/subtle" "encoding/hex" "fmt" "os" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/redis/go-redis/v9" "go.uber.org/zap" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/response" ) // CSRFMiddleware crée un middleware pour la protection CSRF // Utilise Redis pour stocker les tokens CSRF associés aux utilisateurs type CSRFMiddleware struct { redisClient *redis.Client logger *zap.Logger ttl time.Duration // TTL pour les tokens CSRF (défaut: 1 heure) env string // Environnement (development, staging, production) } // NewCSRFMiddleware crée une nouvelle instance du middleware CSRF func NewCSRFMiddleware(redisClient *redis.Client, logger *zap.Logger) *CSRFMiddleware { return &CSRFMiddleware{ redisClient: redisClient, logger: logger, ttl: 1 * time.Hour, // Tokens CSRF valides pendant 1 heure env: "", // Sera défini via SetEnvironment } } // SetEnvironment définit l'environnement pour le middleware func (m *CSRFMiddleware) SetEnvironment(env string) { m.env = env } // Middleware retourne le handler Gin pour la protection CSRF func (m *CSRFMiddleware) Middleware() gin.HandlerFunc { return func(c *gin.Context) { // CSRF can only be disabled via explicit opt-in (dev/test only). // NEVER set CSRF_DISABLED=true in production. if os.Getenv("CSRF_DISABLED") == "true" && (m.env == "development" || m.env == "dev" || m.env == "test") { c.Next() return } // Ignorer GET, HEAD, OPTIONS (méthodes sûres) method := c.Request.Method if method == "GET" || method == "HEAD" || method == "OPTIONS" { c.Next() return } // v0.102: Skip CSRF for API key auth (X-API-Key) - no cookies, stateless if _, hasAPIKey := c.Get("api_key"); hasAPIKey { c.Next() return } // Récupérer le userID depuis le contexte (défini par AuthMiddleware) userIDInterface, exists := c.Get("user_id") if !exists { // Si pas d'utilisateur authentifié, pas besoin de CSRF // (les routes publiques comme login/register sont exclues) c.Next() return } userID, ok := userIDInterface.(uuid.UUID) if !ok { m.logger.Warn("Invalid user_id type in context for CSRF check") c.Next() return } // Récupérer le token CSRF depuis le header token := c.GetHeader("X-CSRF-Token") if token == "" { response.RespondWithAppError(c, apperrors.NewForbiddenError("CSRF token required")) c.Abort() return } // Vérifier le token dans Redis ctx := c.Request.Context() key := m.getCSRFKey(userID) storedToken, err := m.redisClient.Get(ctx, key).Result() if err != nil { if err == redis.Nil { m.logger.Warn("CSRF token not found in Redis", zap.String("user_id", userID.String()), zap.String("ip", c.ClientIP()), ) response.RespondWithAppError(c, apperrors.NewForbiddenError("Invalid or expired CSRF token")) c.Abort() return } m.logger.Error("Redis unavailable for CSRF validation - service temporarily degraded", zap.Error(err), zap.String("user_id", userID.String()), ) response.RespondWithAppError(c, apperrors.NewServiceUnavailableError("Service temporarily unavailable. Please retry later.")) c.Abort() return } // Comparer les tokens (timing-safe pour éviter les attaques par timing) if subtle.ConstantTimeCompare([]byte(storedToken), []byte(token)) != 1 { m.logger.Warn("CSRF token mismatch", zap.String("user_id", userID.String()), zap.String("ip", c.ClientIP()), ) response.RespondWithAppError(c, apperrors.NewForbiddenError("Invalid CSRF token")) c.Abort() return } // Token valide, continuer c.Next() } } // getCSRFKey génère la clé Redis pour un token CSRF func (m *CSRFMiddleware) getCSRFKey(userID uuid.UUID) string { return fmt.Sprintf("csrf:token:%s", userID.String()) } // GenerateToken génère un nouveau token CSRF et le stocke dans Redis func (m *CSRFMiddleware) GenerateToken(ctx context.Context, userID uuid.UUID) (string, error) { // Générer un token aléatoire de 32 bytes (64 caractères hex) tokenBytes := make([]byte, 32) if _, err := rand.Read(tokenBytes); err != nil { return "", fmt.Errorf("failed to generate CSRF token: %w", err) } token := hex.EncodeToString(tokenBytes) // MVP: Si Redis n'est pas disponible, on retourne quand même le token // mais on ne peut pas le stocker pour validation ultérieure. // En développement, le middleware de validation ignorera cette absence. if m.redisClient == nil { if m.env == "development" || m.env == "dev" || m.env == "" { return token, nil } return "", fmt.Errorf("redis client is nil and environment is not development") } // Stocker le token dans Redis avec TTL key := m.getCSRFKey(userID) if err := m.redisClient.Set(ctx, key, token, m.ttl).Err(); err != nil { return "", fmt.Errorf("failed to store CSRF token: %w", err) } return token, nil } // GetToken récupère le token CSRF actuel pour un utilisateur func (m *CSRFMiddleware) GetToken(ctx context.Context, userID uuid.UUID) (string, error) { // Si Redis n'est pas disponible, on en génère un nouveau sans stocker if m.redisClient == nil { return m.GenerateToken(ctx, userID) } key := m.getCSRFKey(userID) token, err := m.redisClient.Get(ctx, key).Result() if err != nil { if err == redis.Nil { // Pas de token existant, en générer un nouveau return m.GenerateToken(ctx, userID) } return "", fmt.Errorf("failed to get CSRF token: %w", err) } return token, nil }