package services import ( "fmt" "os" "time" "veza-backend-api/internal/models" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) type JWTService struct { secretKey []byte issuer string audience string Config *models.JWTConfig } func NewJWTService(secret, issuer, audience string) (*JWTService, error) { if secret == "" { // Fallback to env for safety during transition secret = os.Getenv("JWT_SECRET") if secret == "" { return nil, fmt.Errorf("JWT secret is required") } } if issuer == "" { issuer = "veza-api" } if audience == "" { audience = "veza-app" } // Default config config := &models.JWTConfig{ AccessTokenTTL: 15 * time.Minute, RefreshTokenTTL: 30 * 24 * time.Hour, } return &JWTService{ secretKey: []byte(secret), issuer: issuer, audience: audience, Config: config, }, nil } func (s *JWTService) GenerateAccessToken(user *models.User) (string, error) { claims := models.CustomClaims{ UserID: user.ID, Email: user.Email, Username: user.Username, Role: user.Role, TokenVersion: user.TokenVersion, TokenType: "access", RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.Config.AccessTokenTTL)), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: s.issuer, Audience: jwt.ClaimStrings{s.audience}, ID: uuid.NewString(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(s.secretKey) } func (s *JWTService) GenerateRefreshToken(user *models.User) (string, error) { claims := models.CustomClaims{ UserID: user.ID, TokenVersion: user.TokenVersion, IsRefresh: true, // Mark as refresh token TokenType: "refresh", TokenFamily: uuid.NewString(), // Nouvelle famille de token RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.Config.RefreshTokenTTL)), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: s.issuer, Audience: jwt.ClaimStrings{s.audience}, ID: uuid.NewString(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(s.secretKey) } // GenerateTokenPair génère une paire de tokens (access + refresh) en une seule opération func (s *JWTService) GenerateTokenPair(user *models.User) (*models.TokenPair, error) { // Generate access token accessToken, err := s.GenerateAccessToken(user) if err != nil { return nil, fmt.Errorf("failed to generate access token: %w", err) } // Generate refresh token refreshToken, err := s.GenerateRefreshToken(user) if err != nil { return nil, fmt.Errorf("failed to generate refresh token: %w", err) } return &models.TokenPair{ AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: int(s.Config.AccessTokenTTL.Seconds()), }, nil } // VerifyToken valide et parse un token JWT func (s *JWTService) VerifyToken(tokenString string) (*models.CustomClaims, error) { return s.ValidateToken(tokenString) } // ValidateToken valide un token JWT et retourne les claims func (s *JWTService) ValidateToken(tokenString string) (*models.CustomClaims, error) { // Parse avec validation des claims standards (exp, iat, nbf) ET custom (iss, aud) token, err := jwt.ParseWithClaims(tokenString, &models.CustomClaims{}, func(token *jwt.Token) (interface{}, error) { // Validation stricte de l'algorithme (MOD-P2-002) if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } if token.Method.Alg() != "HS256" { return nil, fmt.Errorf("invalid signing algorithm: %v, expected HS256", token.Method.Alg()) } return s.secretKey, nil }, // Options de validation stricte jwt.WithIssuer(s.issuer), jwt.WithAudience(s.audience), jwt.WithExpirationRequired(), ) if err != nil { return nil, fmt.Errorf("failed to parse token: %w", err) } if claims, ok := token.Claims.(*models.CustomClaims); ok && token.Valid { return claims, nil } return nil, fmt.Errorf("invalid token") } // ParseToken parse un token JWT sans validation complète (utilise ValidateToken) func (s *JWTService) ParseToken(tokenString string) (*models.CustomClaims, error) { return s.ValidateToken(tokenString) } // ExtractClaims extrait les claims d'un token JWT func (s *JWTService) ExtractClaims(tokenString string) (*models.CustomClaims, error) { return s.ValidateToken(tokenString) } // ExtractUserID extrait l'ID utilisateur depuis un token JWT // MIGRATION UUID: retourne uuid.UUID au lieu de int64 func (s *JWTService) ExtractUserID(tokenString string) (uuid.UUID, error) { claims, err := s.ValidateToken(tokenString) if err != nil { return uuid.Nil, fmt.Errorf("failed to extract user ID: %w", err) } return claims.UserID, nil } // VerifyTokenVersion vérifie si la version du token correspond à celle de l'utilisateur func (s *JWTService) VerifyTokenVersion(claims *models.CustomClaims, userTokenVersion int) error { if claims.TokenVersion != userTokenVersion { return fmt.Errorf("token version mismatch: token version %d does not match user version %d", claims.TokenVersion, userTokenVersion) } return nil }