imc-vibe/backend/internal/auth/jwt.go
Christoph Haas 8b2d2649ac Add random password generation for new users
- Move GenerateRandomPassword to auth package for reuse
- Make password optional in user creation (generates 16-char if empty)
- Return generated password in response for user to copy
- Add Generate/Copy UI with show/hide toggle
2026-03-29 14:52:59 +02:00

142 lines
5.4 KiB
Go

// 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)
}