- Changed default AccessTokenTTL from 15 minutes to 5 minutes in jwt_service.go - Updated test mock in mocks_test.go to match new default - All references to AccessTokenTTL automatically use new value - Tests pass successfully - No breaking changes - frontend already handles token refresh - Action 5.1.1.4 complete
178 lines
5.3 KiB
Go
178 lines
5.3 KiB
Go
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
|
|
// Action 5.1.1.4: Reduced access token expiry to 5 minutes for improved security
|
|
config := &models.JWTConfig{
|
|
AccessTokenTTL: 5 * time.Minute,
|
|
RefreshTokenTTL: 30 * 24 * time.Hour,
|
|
}
|
|
|
|
return &JWTService{
|
|
secretKey: []byte(secret),
|
|
issuer: issuer,
|
|
audience: audience,
|
|
Config: config,
|
|
}, nil
|
|
}
|
|
|
|
func (s *JWTService) GetConfig() *models.JWTConfig {
|
|
return s.Config
|
|
}
|
|
|
|
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
|
|
}
|