package api import ( "fmt" "os" "time" "github.com/gin-gonic/gin" authcore "veza-backend-api/internal/core/auth" "veza-backend-api/internal/handlers" "veza-backend-api/internal/repositories" "veza-backend-api/internal/services" "veza-backend-api/internal/validators" ) // setupAuthRoutes configure les routes d'authentification avec toutes les dépendances func (r *APIRouter) setupAuthRoutes(router *gin.RouterGroup) error { // 1. Instanciation des dépendances emailValidator := validators.NewEmailValidator(r.db.GormDB) passwordValidator := validators.NewPasswordValidator() passwordService := services.NewPasswordService(r.db, r.logger) passwordResetService := services.NewPasswordResetService(r.db, r.logger) jwtService, err := services.NewJWTService(r.config.JWTPrivateKeyPath, r.config.JWTPublicKeyPath, r.config.JWTSecret, r.config.JWTIssuer, r.config.JWTAudience) if err != nil { return fmt.Errorf("failed to initialize JWT service: %w", err) } refreshTokenService := services.NewRefreshTokenService(r.db.GormDB) emailVerificationService := services.NewEmailVerificationService(r.db, r.logger) emailService := services.NewEmailService(r.db, r.logger) sessionService := services.NewSessionService(r.db, r.logger) refreshLock := services.NewRefreshLock(r.config.RedisClient) // 2. Service Auth complet authService := authcore.NewAuthService( r.db.GormDB, emailValidator, passwordValidator, passwordService, jwtService, refreshTokenService, emailVerificationService, passwordResetService, emailService, r.config.JobWorker, refreshLock, r.logger, ) // BE-SEC-007: Initialize account lockout service and set it on auth service if r.config.RedisClient != nil { lockoutConfig := &services.AccountLockoutConfig{ MaxAttempts: 5, LockoutDuration: 30 * time.Minute, WindowDuration: 15 * time.Minute, ExemptEmails: r.config.AccountLockoutExemptEmails, } accountLockoutService := services.NewAccountLockoutServiceWithConfig(r.config.RedisClient, r.logger, lockoutConfig) authService.SetAccountLockoutService(accountLockoutService) } else { r.logger.Warn("Redis not available - account lockout disabled") } r.authService = authService // 2.5. User Service for GetMe endpoint userRepo := repositories.NewGormUserRepository(r.db.GormDB) userService := services.NewUserServiceWithDB(userRepo, r.db.GormDB) // 3. Handlers authGroup := router.Group("/auth") { // BE-SEC-005: Apply rate limiting to register endpoint (A04: toujours actif) registerGroup := authGroup.Group("/register") if r.config.EndpointLimiter != nil { registerGroup.Use(r.config.EndpointLimiter.RegisterRateLimit()) } registerGroup.POST("", handlers.Register(authService, sessionService, r.logger, r.config)) // BE-API-001: Initialize 2FA service for login handler twoFactorService := services.NewTwoFactorService(r.db, r.logger) // SFIX-001: Wire TwoFactorService as MFA checker on AuthMiddleware if r.config.AuthMiddleware != nil { r.config.AuthMiddleware.SetTwoFactorChecker(twoFactorService) } // Apply rate limiting to login endpoint (PR-3) loginGroup := authGroup.Group("/login") if r.config.EndpointLimiter != nil { loginGroup.Use(r.config.EndpointLimiter.LoginRateLimit()) } loginGroup.POST("", handlers.Login(authService, sessionService, twoFactorService, r.logger, r.config)) loginGroup.POST("/2fa", handlers.LoginWith2FA(authService, sessionService, twoFactorService, r.logger, r.config)) // SEC-010: Rate limit refresh to prevent token grinding refreshGroup := authGroup.Group("") if r.config.EndpointLimiter != nil { refreshGroup.Use(r.config.EndpointLimiter.RefreshRateLimit()) } refreshGroup.POST("/refresh", handlers.Refresh(authService, sessionService, r.logger, r.config)) // BE-SEC-005: Apply rate limiting to email verification endpoints verifyEmailGroup := authGroup.Group("/verify-email") if r.config.EndpointLimiter != nil { verifyEmailGroup.Use(r.config.EndpointLimiter.VerifyEmailRateLimit()) } verifyEmailGroup.POST("", handlers.VerifyEmail(authService)) resendVerificationGroup := authGroup.Group("/resend-verification") if r.config.EndpointLimiter != nil { resendVerificationGroup.Use(r.config.EndpointLimiter.ResendVerificationRateLimit()) } resendVerificationGroup.POST("", handlers.ResendVerification(authService, r.logger)) // SEC-009: Rate limit check-username to prevent enumeration checkUsernameGroup := authGroup.Group("") if r.config.EndpointLimiter != nil { checkUsernameGroup.Use(r.config.EndpointLimiter.CheckUsernameRateLimit()) } checkUsernameGroup.GET("/check-username", handlers.CheckUsername(authService)) // BE-API-042: OAuth routes (v0.902: CryptoService, redirect validation) oauthCfg := &services.OAuthServiceConfig{ FrontendURL: r.config.FrontendURL, AllowedDomains: r.config.OAuthAllowedRedirectDomains, } if r.config.OAuthEncryptionKey != "" { var cryptoService *services.CryptoService var err error cryptoService, err = services.NewCryptoServiceFromBase64(r.config.OAuthEncryptionKey) if err != nil { // Fallback: use raw bytes if key is long enough keyBytes := []byte(r.config.OAuthEncryptionKey) if len(keyBytes) >= 32 { cryptoService, err = services.NewCryptoService(keyBytes) } } if err != nil { return fmt.Errorf("OAuth CryptoService: %w", err) } if cryptoService != nil { oauthCfg.CryptoService = cryptoService } } oauthService := services.NewOAuthService(r.db, r.logger, jwtService, sessionService, userService, oauthCfg) baseURL := os.Getenv("BASE_URL") if baseURL == "" { appDomain := os.Getenv("APP_DOMAIN") if appDomain == "" { appDomain = "veza.fr" } baseURL = "http://" + appDomain + ":8080" } googleClientID := os.Getenv("OAUTH_GOOGLE_CLIENT_ID") googleClientSecret := os.Getenv("OAUTH_GOOGLE_CLIENT_SECRET") githubClientID := os.Getenv("OAUTH_GITHUB_CLIENT_ID") githubClientSecret := os.Getenv("OAUTH_GITHUB_CLIENT_SECRET") discordClientID := os.Getenv("OAUTH_DISCORD_CLIENT_ID") discordClientSecret := os.Getenv("OAUTH_DISCORD_CLIENT_SECRET") spotifyClientID := os.Getenv("OAUTH_SPOTIFY_CLIENT_ID") spotifyClientSecret := os.Getenv("OAUTH_SPOTIFY_CLIENT_SECRET") hasAnyOAuth := (googleClientID != "" && googleClientSecret != "") || (githubClientID != "" && githubClientSecret != "") || (discordClientID != "" && discordClientSecret != "") || (spotifyClientID != "" && spotifyClientSecret != "") if hasAnyOAuth { oauthService.InitializeConfigs(googleClientID, googleClientSecret, githubClientID, githubClientSecret, discordClientID, discordClientSecret, spotifyClientID, spotifyClientSecret, baseURL) } oauthHandler := handlers.NewOAuthHandler(oauthService, r.logger, r.config.CORSOrigins, r.config.FrontendURL, r.config) oauthGroup := authGroup.Group("/oauth") { oauthGroup.GET("/providers", oauthHandler.GetOAuthProviders) oauthGroup.GET("/:provider", oauthHandler.InitiateOAuth) oauthGroup.GET("/:provider/callback", oauthHandler.OAuthCallback) } // Password reset routes (public) passwordGroup := authGroup.Group("/password") if r.config.EndpointLimiter != nil { passwordGroup.Use(r.config.EndpointLimiter.PasswordResetRateLimit()) } { auditService := services.NewAuditService(r.db, r.logger) passwordGroup.POST("/reset-request", handlers.RequestPasswordReset( passwordResetService, passwordService, emailService, auditService, r.logger, )) passwordGroup.POST("/reset", handlers.ResetPassword( passwordResetService, passwordService, authService, sessionService, auditService, r.logger, )) } // Protected routes (authentification JWT requise) protected := authGroup.Group("") protected.Use(r.config.AuthMiddleware.RequireAuth()) r.applyCSRFProtection(protected) { protected.POST("/logout", handlers.Logout(authService, sessionService, r.logger, r.config)) protected.GET("/me", handlers.GetMe(userService)) // SEC-03: Ephemeral token for HLS/WebSocket (httpOnly cookies prevent direct access) protected.POST("/stream-token", handlers.GenerateStreamToken(userService, jwtService)) twoFactorHandler := handlers.NewTwoFactorHandler(twoFactorService, userService, r.logger) { protected.POST("/2fa/setup", twoFactorHandler.SetupTwoFactor) protected.POST("/2fa/verify", twoFactorHandler.VerifyTwoFactor) protected.POST("/2fa/disable", twoFactorHandler.DisableTwoFactor) protected.GET("/2fa/status", twoFactorHandler.GetTwoFactorStatus) } // F022: WebAuthn/Passkeys routes (v0.13.3) rpID := r.config.AppDomain if rpID == "" { rpID = "localhost" } webauthnService := services.NewWebAuthnService(r.db, r.logger, rpID, "Veza") webauthnHandler := handlers.NewWebAuthnHandler(webauthnService, r.logger) { protected.GET("/passkeys", webauthnHandler.ListPasskeys) protected.POST("/passkeys/register/begin", webauthnHandler.BeginRegistration) protected.POST("/passkeys/register/finish", webauthnHandler.FinishRegistration) protected.PUT("/passkeys/:id", webauthnHandler.RenamePasskey) protected.DELETE("/passkeys/:id", webauthnHandler.DeletePasskey) } // F024/F025: Login history with geolocation (v0.13.3) loginHistoryService := services.NewLoginHistoryService(r.db, r.logger) geoIPService := services.NewGeoIPService(r.logger) loginHistoryService.SetGeoIPResolver(geoIPService) protected.GET("/login-history", handlers.GetLoginHistory(loginHistoryService, r.logger)) } } return nil }