package auth import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "net/http" "strconv" "strings" "time" "go.uber.org/zap" "github.com/kms/api-key-service/internal/config" "github.com/kms/api-key-service/internal/errors" ) // HeaderValidator provides secure validation of authentication headers type HeaderValidator struct { config config.ConfigProvider logger *zap.Logger } // NewHeaderValidator creates a new header validator func NewHeaderValidator(config config.ConfigProvider, logger *zap.Logger) *HeaderValidator { return &HeaderValidator{ config: config, logger: logger, } } // ValidatedUserContext holds validated user information type ValidatedUserContext struct { UserID string Email string Timestamp time.Time Signature string } // ValidateAuthenticationHeaders validates user authentication headers with HMAC signature func (hv *HeaderValidator) ValidateAuthenticationHeaders(r *http.Request) (*ValidatedUserContext, error) { userEmail := r.Header.Get(hv.config.GetString("AUTH_HEADER_USER_EMAIL")) timestamp := r.Header.Get("X-Auth-Timestamp") signature := r.Header.Get("X-Auth-Signature") if userEmail == "" { hv.logger.Warn("Missing user email header") return nil, errors.NewAuthenticationError("User authentication required") } // In development mode, skip signature validation for trusted headers if hv.config.IsDevelopment() { hv.logger.Debug("Development mode: skipping signature validation", zap.String("user_email", userEmail)) } else { // Production mode: require full signature validation if timestamp == "" || signature == "" { hv.logger.Warn("Missing authentication signature headers", zap.String("user_email", userEmail)) return nil, errors.NewAuthenticationError("Authentication signature required") } // Validate timestamp (prevent replay attacks) timestampInt, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { hv.logger.Warn("Invalid timestamp format", zap.String("timestamp", timestamp), zap.String("user_email", userEmail)) return nil, errors.NewAuthenticationError("Invalid timestamp format") } timestampTime := time.Unix(timestampInt, 0) now := time.Now() // Allow 5 minutes clock skew maxAge := 5 * time.Minute if now.Sub(timestampTime) > maxAge || timestampTime.After(now.Add(1*time.Minute)) { hv.logger.Warn("Timestamp outside acceptable window", zap.Time("timestamp", timestampTime), zap.Time("now", now), zap.String("user_email", userEmail)) return nil, errors.NewAuthenticationError("Request timestamp outside acceptable window") } // Validate HMAC signature if !hv.validateSignature(userEmail, timestamp, signature) { hv.logger.Warn("Invalid authentication signature", zap.String("user_email", userEmail)) return nil, errors.NewAuthenticationError("Invalid authentication signature") } } // Validate email format if !hv.isValidEmail(userEmail) { hv.logger.Warn("Invalid email format", zap.String("user_email", userEmail)) return nil, errors.NewAuthenticationError("Invalid email format") } hv.logger.Debug("Authentication headers validated successfully", zap.String("user_email", userEmail)) // Set defaults for development mode var timestampTime time.Time var signatureValue string if hv.config.IsDevelopment() { timestampTime = time.Now() signatureValue = "dev-mode-bypass" } else { timestampInt, _ := strconv.ParseInt(timestamp, 10, 64) timestampTime = time.Unix(timestampInt, 0) signatureValue = signature } return &ValidatedUserContext{ UserID: userEmail, Email: userEmail, Timestamp: timestampTime, Signature: signatureValue, }, nil } // validateSignature validates the HMAC signature func (hv *HeaderValidator) validateSignature(userEmail, timestamp, signature string) bool { // Get the signing key from config signingKey := hv.config.GetString("AUTH_SIGNING_KEY") if signingKey == "" { hv.logger.Error("AUTH_SIGNING_KEY not configured") return false } // Create the signing string signingString := fmt.Sprintf("%s:%s", userEmail, timestamp) // Calculate expected signature mac := hmac.New(sha256.New, []byte(signingKey)) mac.Write([]byte(signingString)) expectedSignature := hex.EncodeToString(mac.Sum(nil)) // Use constant-time comparison to prevent timing attacks return hmac.Equal([]byte(signature), []byte(expectedSignature)) } // isValidEmail performs basic email validation func (hv *HeaderValidator) isValidEmail(email string) bool { if len(email) == 0 || len(email) > 254 { return false } // Basic email validation - contains @ and has valid structure parts := strings.Split(email, "@") if len(parts) != 2 { return false } local, domain := parts[0], parts[1] // Local part validation if len(local) == 0 || len(local) > 64 { return false } // Domain part validation if len(domain) == 0 || len(domain) > 253 { return false } if !strings.Contains(domain, ".") { return false } // Check for invalid characters (basic check) invalidChars := []string{" ", "..", "@@", "<", ">", "\"", "'"} for _, char := range invalidChars { if strings.Contains(email, char) { return false } } return true } // GenerateSignatureExample generates an example signature for documentation func (hv *HeaderValidator) GenerateSignatureExample(userEmail string, timestamp string, signingKey string) string { signingString := fmt.Sprintf("%s:%s", userEmail, timestamp) mac := hmac.New(sha256.New, []byte(signingKey)) mac.Write([]byte(signingString)) return hex.EncodeToString(mac.Sum(nil)) }