// Package auth gère le hachage des mots de passe, la génération/validation // des JWT et des refresh tokens. package auth import ( "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) // ErrInvalidToken est renvoyé quand un access token est invalide ou expiré. var ErrInvalidToken = errors.New("token invalide") // Manager encapsule la logique d'authentification (secret JWT, durées de vie). type Manager struct { secret []byte accessTTL time.Duration refreshTTL time.Duration } // NewManager crée un gestionnaire d'authentification. func NewManager(secret []byte, accessTTL, refreshTTL time.Duration) *Manager { return &Manager{secret: secret, accessTTL: accessTTL, refreshTTL: refreshTTL} } // HashPassword retourne le hash bcrypt d'un mot de passe en clair. func HashPassword(password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", err } return string(hash), nil } // CheckPassword compare un mot de passe en clair avec son hash bcrypt. func CheckPassword(hash, password string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil } // Claims représente le contenu d'un access token. type Claims struct { UserID string `json:"uid"` jwt.RegisteredClaims } // GenerateAccessToken crée un access token JWT signé pour l'utilisateur donné. func (m *Manager) GenerateAccessToken(userID string) (string, error) { now := time.Now() claims := Claims{ UserID: userID, RegisteredClaims: jwt.RegisteredClaims{ Subject: userID, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(m.accessTTL)), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(m.secret) } // ParseAccessToken valide un access token et retourne ses claims. func (m *Manager) ParseAccessToken(tokenString string) (*Claims, error) { claims := &Claims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("méthode de signature inattendue : %v", t.Header["alg"]) } return m.secret, nil }) if err != nil || !token.Valid { return nil, ErrInvalidToken } return claims, nil } // GenerateRefreshToken génère un refresh token opaque (aléatoire) et son hash // SHA-256 (pour stockage en base), ainsi que sa date d'expiration. func (m *Manager) GenerateRefreshToken() (token, hash string, expiresAt time.Time, err error) { raw := make([]byte, 32) if _, err = rand.Read(raw); err != nil { return "", "", time.Time{}, err } // On préfixe d'un uuid pour garantir l'unicité et faciliter le debug. token = uuid.NewString() + "." + base64.RawURLEncoding.EncodeToString(raw) hash = HashToken(token) expiresAt = time.Now().Add(m.refreshTTL) return token, hash, expiresAt, nil } // HashToken retourne le hash SHA-256 hexadécimal d'un token. // SHA-256 suffit pour un secret à forte entropie (contrairement aux mots de passe). func HashToken(token string) string { sum := sha256.Sum256([]byte(token)) return hex.EncodeToString(sum[:]) } // AccessTTL expose la durée de vie des access tokens. func (m *Manager) AccessTTL() time.Duration { return m.accessTTL }