package security import ( "errors" "net/http" "strings" "time" "github.com/golang-jwt/jwt/v5" "github.com/labstack/echo/v5" "trankilou.fr/lassistanoque/backend/internal/config" "trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/service/auth" ) // Claims personnalisées embarquées dans le JWT. type Claims struct { UserID string `json:"uid"` Email string `json:"email"` Firstname string `json:"firstname"` Lastname string `json:"lastname"` Administrator bool `json:"administrator"` jwt.RegisteredClaims } type JwtTokenManager struct { secret []byte accessTTL time.Duration refreshTTL time.Duration issuer string } func NewJwtTokenManager(accessTTL, refreshTTL time.Duration, issuer string) *JwtTokenManager { cfg := config.GetConfig() return &JwtTokenManager{ secret: []byte(cfg.JWTSecret), accessTTL: accessTTL, refreshTTL: refreshTTL, issuer: issuer, } } // GenerateAccessToken crée un JWT de courte durée (ex: 15 min) utilisé // pour authentifier les requêtes API. func (tm *JwtTokenManager) GenerateAccessToken(user *domain.User) (string, time.Time, error) { expiresAt := time.Now().Add(tm.accessTTL) claims := Claims{ UserID: user.ID, Email: user.Email, Firstname: user.Firstname, Lastname: user.Lastname, Administrator: user.Administrator, RegisteredClaims: jwt.RegisteredClaims{ Issuer: tm.issuer, Subject: user.ID, ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString(tm.secret) return signed, expiresAt, err } // GenerateRefreshToken crée un token de longue durée (ex: 7 jours) utilisé // uniquement pour obtenir un nouvel access token, jamais pour appeler l'API directement. func (tm *JwtTokenManager) GenerateRefreshToken(userID string) (string, time.Time, error) { expiresAt := time.Now().Add(tm.refreshTTL) claims := jwt.RegisteredClaims{ Issuer: tm.issuer, Subject: userID, ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(time.Now()), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString(tm.secret) return signed, expiresAt, err } // ParseAndValidate décode et vérifie la signature + l'expiration d'un JWT. func (tm *JwtTokenManager) ParseAndValidate(tokenString string) (*domain.User, error) { claims := &Claims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("méthode de signature inattendue") } return tm.secret, nil }) if err != nil { return nil, err } if !token.Valid { return nil, errors.New("token invalide") } return &domain.User{ ID: claims.UserID, Email: claims.Email, Firstname: claims.Firstname, Lastname: claims.Lastname, Administrator: claims.Administrator, }, nil } func (tm *JwtTokenManager) TokenMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { header := c.Request().Header.Get("Authorization") if header == "" { return echo.NewHTTPError(http.StatusUnauthorized, "en-tête Authorization manquant") } parts := strings.SplitN(header, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { return echo.NewHTTPError(http.StatusUnauthorized, "format attendu: Bearer ") } claims, err := tm.ParseAndValidate(parts[1]) if err != nil { return echo.NewHTTPError(http.StatusUnauthorized, "token invalide ou expiré") } // Rend les infos disponibles aux handlers suivants via c.Get(...) c.Set(auth.ContextUserIDKey, claims.ID) c.Set(auth.ContextEmailKey, claims.Email) c.Set(auth.ContextNameKey, claims.Firstname+" "+claims.Lastname) c.Set(auth.ContextAdminKey, claims.Administrator) return next(c) } }