✨ feat: implement user authentication system with JWT and PostgreSQL
Added comprehensive user management system: - User registration with validation (3-50 char username, 6+ char password) - JWT-based authentication with bcrypt password hashing - Admin authentication with master password - Password reset workflow with admin flagging - PostgreSQL repository implementation - SQLite repository for testing - Unified authentication service interface API Endpoints: - POST /api/v1/auth/register - User registration - POST /api/v1/auth/login - User/admin authentication - POST /api/v1/auth/password-reset/request - Request password reset - POST /api/v1/auth/password-reset/complete - Complete password reset - POST /api/v1/auth/validate - JWT token validation Security Features: - Password hashing with bcrypt - JWT token generation and validation - Admin claims in JWT tokens - Configurable token expiration - Input validation for all endpoints Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
235
pkg/user/auth_service.go
Normal file
235
pkg/user/auth_service.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// JWTConfig holds JWT configuration
|
||||
type JWTConfig struct {
|
||||
Secret string
|
||||
ExpirationTime time.Duration
|
||||
Issuer string
|
||||
}
|
||||
|
||||
// userServiceImpl implements the unified UserService interface
|
||||
type userServiceImpl struct {
|
||||
repo UserRepository
|
||||
jwtConfig JWTConfig
|
||||
masterPassword string
|
||||
}
|
||||
|
||||
// NewUserService creates a new user service with all functionality
|
||||
func NewUserService(repo UserRepository, jwtConfig JWTConfig, masterPassword string) *userServiceImpl {
|
||||
return &userServiceImpl{
|
||||
repo: repo,
|
||||
jwtConfig: jwtConfig,
|
||||
masterPassword: masterPassword,
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate authenticates a user with username and password
|
||||
func (s *userServiceImpl) Authenticate(ctx context.Context, username, password string) (*User, error) {
|
||||
user, err := s.repo.GetUserByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
if user == nil {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Check password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Update last login time
|
||||
now := time.Now()
|
||||
user.LastLogin = &now
|
||||
if err := s.repo.UpdateUser(ctx, user); err != nil {
|
||||
// Don't fail authentication if we can't update last login
|
||||
// Just log it and continue
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GenerateJWT generates a JWT token for the given user
|
||||
func (s *userServiceImpl) GenerateJWT(ctx context.Context, user *User) (string, error) {
|
||||
// Create the claims
|
||||
claims := jwt.MapClaims{
|
||||
"sub": user.ID,
|
||||
"name": user.Username,
|
||||
"admin": user.IsAdmin,
|
||||
"exp": time.Now().Add(s.jwtConfig.ExpirationTime).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"iss": s.jwtConfig.Issuer,
|
||||
}
|
||||
|
||||
// Create token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
// Sign and get the complete encoded token as a string
|
||||
tokenString, err := token.SignedString([]byte(s.jwtConfig.Secret))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign JWT: %w", err)
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// ValidateJWT validates a JWT token and returns the user
|
||||
func (s *userServiceImpl) ValidateJWT(ctx context.Context, tokenString string) (*User, error) {
|
||||
// Parse the token
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
// Verify the signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(s.jwtConfig.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JWT: %w", err)
|
||||
}
|
||||
|
||||
// Check if token is valid
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid JWT token")
|
||||
}
|
||||
|
||||
// Get claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid JWT claims")
|
||||
}
|
||||
|
||||
// Get user ID from claims
|
||||
userIDFloat, ok := claims["sub"].(float64)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid user ID in JWT")
|
||||
}
|
||||
|
||||
userID := uint(userIDFloat)
|
||||
|
||||
// Get user from repository
|
||||
user, err := s.repo.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user from JWT: %w", err)
|
||||
}
|
||||
if user == nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// HashPassword hashes a password using bcrypt (implements PasswordService interface)
|
||||
func (s *userServiceImpl) HashPassword(ctx context.Context, password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// AdminAuthenticate authenticates an admin user with master password
|
||||
func (s *userServiceImpl) AdminAuthenticate(ctx context.Context, masterPassword string) (*User, error) {
|
||||
// Check if master password matches
|
||||
if masterPassword != s.masterPassword {
|
||||
return nil, errors.New("invalid admin credentials")
|
||||
}
|
||||
|
||||
// Create a virtual admin user (not persisted)
|
||||
adminUser := &User{
|
||||
ID: 0, // Special ID for admin
|
||||
Username: "admin",
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
return adminUser, nil
|
||||
}
|
||||
|
||||
// UserExists checks if a user exists by username
|
||||
func (s *userServiceImpl) UserExists(ctx context.Context, username string) (bool, error) {
|
||||
return s.repo.UserExists(ctx, username)
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in the database
|
||||
func (s *userServiceImpl) CreateUser(ctx context.Context, user *User) error {
|
||||
return s.repo.CreateUser(ctx, user)
|
||||
}
|
||||
|
||||
// RequestPasswordReset requests a password reset for a user
|
||||
func (s *userServiceImpl) RequestPasswordReset(ctx context.Context, username string) error {
|
||||
// Check if user exists
|
||||
exists, err := s.repo.UserExists(ctx, username)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if user exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("user not found: %s", username)
|
||||
}
|
||||
|
||||
// Allow password reset
|
||||
return s.repo.AllowPasswordReset(ctx, username)
|
||||
}
|
||||
|
||||
// CompletePasswordReset completes the password reset process
|
||||
func (s *userServiceImpl) CompletePasswordReset(ctx context.Context, username, newPassword string) error {
|
||||
// Hash the new password
|
||||
hashedPassword, err := s.HashPassword(ctx, newPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash new password: %w", err)
|
||||
}
|
||||
|
||||
// Complete the password reset
|
||||
return s.repo.CompletePasswordReset(ctx, username, hashedPassword)
|
||||
}
|
||||
|
||||
// PasswordResetServiceImpl implements the PasswordResetService interface
|
||||
type PasswordResetServiceImpl struct {
|
||||
repo UserRepository
|
||||
auth *userServiceImpl
|
||||
}
|
||||
|
||||
// NewPasswordResetService creates a new password reset service
|
||||
func NewPasswordResetService(repo UserRepository, auth *userServiceImpl) *PasswordResetServiceImpl {
|
||||
return &PasswordResetServiceImpl{
|
||||
repo: repo,
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
// RequestPasswordReset requests a password reset for a user
|
||||
func (s *PasswordResetServiceImpl) RequestPasswordReset(ctx context.Context, username string) error {
|
||||
// Check if user exists
|
||||
exists, err := s.repo.UserExists(ctx, username)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if user exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("user not found: %s", username)
|
||||
}
|
||||
|
||||
// Allow password reset
|
||||
return s.repo.AllowPasswordReset(ctx, username)
|
||||
}
|
||||
|
||||
// CompletePasswordReset completes the password reset process
|
||||
func (s *PasswordResetServiceImpl) CompletePasswordReset(ctx context.Context, username, newPassword string) error {
|
||||
// Hash the new password
|
||||
hashedPassword, err := s.auth.HashPassword(ctx, newPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash new password: %w", err)
|
||||
}
|
||||
|
||||
// Complete the password reset
|
||||
return s.repo.CompletePasswordReset(ctx, username, hashedPassword)
|
||||
}
|
||||
Reference in New Issue
Block a user