- 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
142 lines
5.4 KiB
Go
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)
|
|
}
|