SEC-03: TokenStorage.getAccessToken() returns null with httpOnly cookies. New POST /api/v1/auth/stream-token returns a 5-min JWT compatible with both stream server (Claims struct) and chat server (JwtClaims struct). Frontend hlsService and websocket updated to use fetchStreamToken() fallback.
198 lines
7.5 KiB
Go
198 lines
7.5 KiB
Go
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.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)
|
|
|
|
// 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
|
|
jwtSecretBytes := []byte(r.config.JWTSecret)
|
|
oauthService := services.NewOAuthService(r.db, r.logger, jwtSecretBytes)
|
|
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)
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|