🧪 test: implement JWT secret rotation BDD tests
- Fix admin handler to handle flexible boolean parsing - Modify GenerateJWT to use latest secret for signing - Update JWT secret manager for proper expiration handling - Fix BDD test steps to use actual tokens instead of hardcoded ones - Add comprehensive debug logging for JWT operations Resolves JWT secret rotation feature implementation Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -22,6 +23,7 @@ type userServiceImpl struct {
|
||||
repo UserRepository
|
||||
jwtConfig JWTConfig
|
||||
masterPassword string
|
||||
secretManager *JWTSecretManager
|
||||
}
|
||||
|
||||
// NewUserService creates a new user service with all functionality
|
||||
@@ -30,6 +32,7 @@ func NewUserService(repo UserRepository, jwtConfig JWTConfig, masterPassword str
|
||||
repo: repo,
|
||||
jwtConfig: jwtConfig,
|
||||
masterPassword: masterPassword,
|
||||
secretManager: NewJWTSecretManager(jwtConfig.Secret),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,38 +77,77 @@ func (s *userServiceImpl) GenerateJWT(ctx context.Context, user *User) (string,
|
||||
// Create token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
// Get all valid secrets and use the most recently added one for signing
|
||||
// This supports JWT secret rotation by signing new tokens with the latest secret
|
||||
validSecrets := s.secretManager.GetAllValidSecrets()
|
||||
if len(validSecrets) == 0 {
|
||||
return "", errors.New("no valid JWT secrets available")
|
||||
}
|
||||
|
||||
// Use the most recently added secret (last in the list)
|
||||
// This ensures new tokens are signed with the latest secret
|
||||
signingSecret := validSecrets[len(validSecrets)-1].Secret
|
||||
log.Trace().Ctx(ctx).Str("signing_secret", signingSecret).Bool("is_primary", validSecrets[len(validSecrets)-1].IsPrimary).Msg("Generating JWT with latest secret")
|
||||
|
||||
// Sign and get the complete encoded token as a string
|
||||
tokenString, err := token.SignedString([]byte(s.jwtConfig.Secret))
|
||||
tokenString, err := token.SignedString([]byte(signingSecret))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign JWT: %w", err)
|
||||
}
|
||||
|
||||
log.Trace().Ctx(ctx).Str("token", tokenString).Msg("Generated JWT token")
|
||||
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"])
|
||||
}
|
||||
log.Trace().Ctx(ctx).Str("token", tokenString).Msg("Validating JWT token")
|
||||
|
||||
return []byte(s.jwtConfig.Secret), nil
|
||||
})
|
||||
// Get all valid secrets for validation
|
||||
validSecrets := s.secretManager.GetAllValidSecrets()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JWT: %w", err)
|
||||
log.Trace().Ctx(ctx).Int("num_secrets", len(validSecrets)).Msg("Validating JWT with multiple secrets")
|
||||
for i, secret := range validSecrets {
|
||||
log.Trace().Ctx(ctx).Int("secret_index", i).Str("secret", secret.Secret).Bool("is_primary", secret.IsPrimary).Msg("Trying secret")
|
||||
}
|
||||
|
||||
// Check if token is valid
|
||||
if !token.Valid {
|
||||
// Try each valid secret until we find one that works
|
||||
var parsedToken *jwt.Token
|
||||
var validationError error
|
||||
|
||||
for i, secret := range validSecrets {
|
||||
// Parse the token with current secret
|
||||
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(secret.Secret), nil
|
||||
})
|
||||
|
||||
if err == nil && token.Valid {
|
||||
log.Trace().Ctx(ctx).Int("secret_index", i).Str("secret", secret.Secret).Msg("JWT validation successful")
|
||||
parsedToken = token
|
||||
break
|
||||
}
|
||||
|
||||
// Store the last error for reporting
|
||||
validationError = err
|
||||
if err != nil {
|
||||
log.Trace().Ctx(ctx).Int("secret_index", i).Str("secret", secret.Secret).Err(err).Msg("JWT validation failed")
|
||||
}
|
||||
}
|
||||
|
||||
if parsedToken == nil {
|
||||
if validationError != nil {
|
||||
return nil, fmt.Errorf("failed to parse JWT: %w", validationError)
|
||||
}
|
||||
return nil, errors.New("invalid JWT token")
|
||||
}
|
||||
|
||||
// Get claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
claims, ok := parsedToken.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid JWT claims")
|
||||
}
|
||||
@@ -156,6 +198,21 @@ func (s *userServiceImpl) AdminAuthenticate(ctx context.Context, masterPassword
|
||||
return adminUser, nil
|
||||
}
|
||||
|
||||
// AddJWTSecret adds a new JWT secret to the manager
|
||||
func (s *userServiceImpl) AddJWTSecret(secret string, isPrimary bool, expiresIn time.Duration) {
|
||||
s.secretManager.AddSecret(secret, isPrimary, expiresIn)
|
||||
}
|
||||
|
||||
// RotateJWTSecret rotates to a new primary JWT secret
|
||||
func (s *userServiceImpl) RotateJWTSecret(newSecret string) {
|
||||
s.secretManager.RotateToSecret(newSecret)
|
||||
}
|
||||
|
||||
// GetJWTSecretByIndex returns a JWT secret by index for testing
|
||||
func (s *userServiceImpl) GetJWTSecretByIndex(index int) (string, bool) {
|
||||
return s.secretManager.GetSecretByIndex(index)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user