// Package auth gère les tokens JWT RS256 pour les sessions utilisateurs. // Les clés RSA sont générées automatiquement au premier démarrage et stockées sur disque. package auth import ( "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "errors" "fmt" "os" "path/filepath" "time" "github.com/golang-jwt/jwt/v5" ) const ( accessTokenDuration = 15 * time.Minute refreshTokenDuration = 7 * 24 * time.Hour rsaKeySize = 2048 ) // Claims représente le contenu d'un JWT d'accès ProxmoxPanel. type Claims struct { UserID int64 `json:"uid"` Username string `json:"sub"` IsAdmin bool `json:"admin"` jwt.RegisteredClaims } // JWTManager gère la signature et la vérification des tokens JWT. type JWTManager struct { privateKey *rsa.PrivateKey publicKey *rsa.PublicKey } // NewJWTManager charge ou génère les clés RSA, et retourne un JWTManager prêt à l'emploi. func NewJWTManager(dataDir string) (*JWTManager, error) { keysDir := filepath.Join(dataDir, "keys") if err := os.MkdirAll(keysDir, 0700); err != nil { return nil, fmt.Errorf("création répertoire clés : %w", err) } privPath := filepath.Join(keysDir, "jwt.key") pubPath := filepath.Join(keysDir, "jwt.pub") var privKey *rsa.PrivateKey if _, err := os.Stat(privPath); os.IsNotExist(err) { // Générer une paire de clés RSA-2048 privKey, err = rsa.GenerateKey(rand.Reader, rsaKeySize) if err != nil { return nil, fmt.Errorf("génération clés RSA : %w", err) } // Sauvegarder la clé privée en PEM privPEM := pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privKey), }) if err := os.WriteFile(privPath, privPEM, 0600); err != nil { return nil, fmt.Errorf("sauvegarde clé privée : %w", err) } // Sauvegarder la clé publique en PEM pubBytes, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey) if err != nil { return nil, fmt.Errorf("export clé publique : %w", err) } pubPEM := pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: pubBytes, }) if err := os.WriteFile(pubPath, pubPEM, 0644); err != nil { return nil, fmt.Errorf("sauvegarde clé publique : %w", err) } } else { // Charger la clé privée existante privPEM, err := os.ReadFile(privPath) if err != nil { return nil, fmt.Errorf("lecture clé privée : %w", err) } block, _ := pem.Decode(privPEM) if block == nil { return nil, errors.New("clé privée invalide (PEM)") } privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("parsing clé privée : %w", err) } } return &JWTManager{ privateKey: privKey, publicKey: &privKey.PublicKey, }, nil } // GenerateAccessToken crée un JWT d'accès signé RS256 (durée : 15 min). func (m *JWTManager) GenerateAccessToken(userID int64, username string, isAdmin bool) (string, error) { claims := Claims{ UserID: userID, Username: username, IsAdmin: isAdmin, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(accessTokenDuration)), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: "proxmoxpanel", }, } token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) return token.SignedString(m.privateKey) } // GenerateRefreshToken crée un token de renouvellement (durée : 7 jours). // Ce token est plus simple — il ne contient que le userID et l'expiration. func (m *JWTManager) GenerateRefreshToken(userID int64) (string, error) { claims := jwt.RegisteredClaims{ Subject: fmt.Sprintf("%d", userID), ExpiresAt: jwt.NewNumericDate(time.Now().Add(refreshTokenDuration)), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: "proxmoxpanel-refresh", } token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) return token.SignedString(m.privateKey) } // ValidateAccessToken vérifie et décode un JWT d'accès. func (m *JWTManager) ValidateAccessToken(tokenStr string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { return nil, fmt.Errorf("méthode de signature inattendue : %v", t.Header["alg"]) } return m.publicKey, nil }) if err != nil { return nil, fmt.Errorf("validation token : %w", err) } claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { return nil, errors.New("token invalide") } return claims, nil } // ValidateRefreshToken vérifie un token de renouvellement et retourne le userID. func (m *JWTManager) ValidateRefreshToken(tokenStr string) (int64, error) { token, err := jwt.ParseWithClaims(tokenStr, &jwt.RegisteredClaims{}, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { return nil, fmt.Errorf("méthode de signature inattendue : %v", t.Header["alg"]) } return m.publicKey, nil }) if err != nil { return 0, fmt.Errorf("validation refresh token : %w", err) } claims, ok := token.Claims.(*jwt.RegisteredClaims) if !ok || !token.Valid { return 0, errors.New("refresh token invalide") } if claims.Issuer != "proxmoxpanel-refresh" { return 0, errors.New("émetteur token invalide") } var userID int64 fmt.Sscanf(claims.Subject, "%d", &userID) return userID, nil } // RefreshTokenDuration retourne la durée de validité du refresh token. func RefreshTokenDuration() time.Duration { return refreshTokenDuration }