-
This commit is contained in:
171
internal/auth/header_validator.go
Normal file
171
internal/auth/header_validator.go
Normal file
@ -0,0 +1,171 @@
|
||||
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")
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
return &ValidatedUserContext{
|
||||
UserID: userEmail,
|
||||
Email: userEmail,
|
||||
Timestamp: timestampTime,
|
||||
Signature: signature,
|
||||
}, 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))
|
||||
}
|
||||
@ -57,6 +57,12 @@ func (j *JWTManager) GenerateToken(userToken *domain.UserToken) (string, error)
|
||||
return "", errors.NewValidationError("JWT secret not configured")
|
||||
}
|
||||
|
||||
// Generate secure JWT ID
|
||||
jti := j.generateJTI()
|
||||
if jti == "" {
|
||||
return "", errors.NewInternalError("Failed to generate secure JWT ID - cryptographic random number generation failed")
|
||||
}
|
||||
|
||||
// Create custom claims
|
||||
claims := CustomClaims{
|
||||
UserID: userToken.UserID,
|
||||
@ -72,7 +78,7 @@ func (j *JWTManager) GenerateToken(userToken *domain.UserToken) (string, error)
|
||||
ExpiresAt: jwt.NewNumericDate(userToken.ExpiresAt),
|
||||
IssuedAt: jwt.NewNumericDate(userToken.IssuedAt),
|
||||
NotBefore: jwt.NewNumericDate(userToken.IssuedAt),
|
||||
ID: j.generateJTI(),
|
||||
ID: jti,
|
||||
},
|
||||
}
|
||||
|
||||
@ -272,8 +278,10 @@ func (j *JWTManager) IsTokenRevoked(tokenString string) (bool, error) {
|
||||
func (j *JWTManager) generateJTI() string {
|
||||
bytes := make([]byte, 16)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
// Fallback to timestamp-based ID if random generation fails
|
||||
return fmt.Sprintf("jti_%d", time.Now().UnixNano())
|
||||
// Log the error and fail securely - do not generate predictable fallback IDs
|
||||
j.logger.Error("Cryptographic random number generation failed - cannot generate secure JWT ID", zap.Error(err))
|
||||
// Return an error indicator that will cause token generation to fail
|
||||
return ""
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user