// Package auth handles authentication and authorization for the application. // It provides JWT token management and password hashing. package auth import ( "crypto/rand" // cryptographically secure random number generator "errors" // standard errors package "log" // logging "time" // time handling "github.com/golang-jwt/jwt/v5" // JWT library for token handling "golang.org/x/crypto/bcrypt" // bcrypt for secure password hashing ) // Custom error types for JWT operations. var ( ErrInvalidToken = errors.New("invalid token") // Token is malformed or has invalid signature ErrExpiredToken = errors.New("token has expired") // Token has expired ) // Claims represents the data stored in a JWT token. // JWT tokens are signed payloads that can be verified but not tampered with. type Claims struct { UserID uint `json:"userId"` // ID of the logged-in user Username string `json:"username"` // Username for display Role string `json:"role"` // User's role ("admin" or "user") jwt.RegisteredClaims // Standard JWT claims (expiration, issuer, etc.) } // JWTManager handles creating and validating JWT tokens. type JWTManager struct { secretKey []byte // Secret key for signing/verifying tokens (keep this secret!) issuer string // Token issuer (used to verify token source) } // NewJWTManager creates a new JWT manager. // secretKey: the secret key for signing tokens. Should be long and random. // issuer: the token issuer string (usually the app name). func NewJWTManager(secretKey, issuer string) *JWTManager { return &JWTManager{ secretKey: []byte(secretKey), // Convert string to bytes for signing issuer: issuer, } } // GenerateToken creates a new JWT token for a user. // userID: the user's ID to include in the token. // username: the username to include for display. // role: the user's role ("admin" or "user"). // duration: how long the token should be valid. // Returns: the signed token string and any error. func (j *JWTManager) GenerateToken(userID uint, username, role string, duration time.Duration) (string, error) { // Create the claims (payload) for the token. claims := &Claims{ UserID: userID, Username: username, Role: role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)), // Token expires in "duration" IssuedAt: jwt.NewNumericDate(time.Now()), // When the token was created Issuer: j.issuer, // Who issued the token }, } // Create a new token with HMAC-SHA256 signing. // HMAC is a symmetric algorithm - same key for signing and verifying. token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Sign the token with our secret key and return the string representation. return token.SignedString(j.secretKey) } // ValidateToken checks if a token is valid and returns its claims. // tokenString: the JWT token string to validate. // Returns: the claims if valid, or an error if invalid. func (j *JWTManager) ValidateToken(tokenString string) (*Claims, error) { // Parse and validate the token. // The callback function is called to get the signing key. token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { // Verify the signing method is HMAC (not RSA, ECDSA, etc.). // This prevents algorithm confusion attacks. if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, ErrInvalidToken } return j.secretKey, nil // Return our secret key for verification }) // Handle parsing errors. if err != nil { // Check specifically for expiration. if errors.Is(err, jwt.ErrTokenExpired) { return nil, ErrExpiredToken } return nil, ErrInvalidToken } // Extract and validate claims. claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { // token.Valid is false if signature verification failed return nil, ErrInvalidToken } return claims, nil } // HashPassword creates a bcrypt hash of a password. // password: the plain-text password to hash. // Returns: the bcrypt hash and any error. // Uses bcrypt's default cost factor (currently 10). func HashPassword(password string) (string, error) { // GenerateFromPassword creates a bcrypt hash. // bcrypt is designed to be slow to resist brute-force attacks. // DefaultCost is a good balance between security and performance. hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", err } return string(hash), nil // Convert bytes to string } // CheckPassword verifies a password against a bcrypt hash. // password: the plain-text password to check. // hash: the bcrypt hash to verify against. // Returns: true if the password matches, false otherwise. func CheckPassword(password, hash string) bool { // CompareHashAndPassword returns nil if they match, error otherwise. err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } // GenerateRandomPassword creates a cryptographically secure random password. func GenerateRandomPassword(length int) string { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*" result := make([]byte, length) if _, err := rand.Read(result); err != nil { log.Fatalf("Failed to generate random password: %v", err) } for i := range result { result[i] = charset[int(result[i])%len(charset)] } return string(result) }