// Package auth provides JWT token generation and validation for the Cyrene platform. package auth import ( "crypto/rand" "encoding/hex" "fmt" "time" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) // Claims represents the JWT claims for a Cyrene user. type Claims struct { jwt.RegisteredClaims UserID string `json:"user_id"` Username string `json:"username"` Role string `json:"role"` // "admin" | "user" } // TokenManager handles JWT creation and validation. type TokenManager struct { secret []byte expiry time.Duration } // NewTokenManager creates a new TokenManager. // secret must be at least 32 characters. func NewTokenManager(secret string, expiryHours int) *TokenManager { return &TokenManager{ secret: []byte(secret), expiry: time.Duration(expiryHours) * time.Hour, } } // Generate creates a signed JWT token for the given user. func (tm *TokenManager) Generate(userID, username, role string) (string, error) { now := time.Now() claims := &Claims{ RegisteredClaims: jwt.RegisteredClaims{ Issuer: "cyrene", Subject: userID, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(tm.expiry)), ID: generateTokenID(), }, UserID: userID, Username: username, Role: role, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(tm.secret) } // Validate parses and validates a JWT token string. // Returns the Claims if valid, or an error. func (tm *TokenManager) Validate(tokenStr string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (any, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return tm.secret, nil }) if err != nil { return nil, fmt.Errorf("parse token: %w", err) } claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { return nil, fmt.Errorf("invalid token") } return claims, nil } // IsAdmin checks if the claims belong to an admin user. func (c *Claims) IsAdmin() bool { return c.Role == "admin" } // HashPassword creates a bcrypt hash of the password. func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", fmt.Errorf("hash password: %w", err) } return string(bytes), nil } // CheckPassword compares a password with its bcrypt hash. func CheckPassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } func generateTokenID() string { b := make([]byte, 16) rand.Read(b) return hex.EncodeToString(b) }