7cb2756b67
Add user-based rate limiting middleware (100 req/min) for authenticated endpoints using httprate library. Implement common password validation blocking 50+ weak passwords. Improve test coverage from 38.3% to 44.6% with comprehensive refresh token tests. Security improvements: - Rate limiting by user ID for /habits and /stats endpoints - X-RateLimit-Limit header in responses - Common password blacklist in password validation - Refresh token test suite with 5 scenarios (valid, invalid, expired, revoked, empty)
132 lines
3.1 KiB
Go
132 lines
3.1 KiB
Go
package validation
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
// RFC 5322 compliant email regex (simplified but robust)
|
|
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9.!#$%&'*+/=?^_` + "`" + `{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`)
|
|
|
|
type ValidationError struct {
|
|
Field string
|
|
Message string
|
|
}
|
|
|
|
func (e ValidationError) Error() string {
|
|
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
|
}
|
|
|
|
func ValidateEmail(email string) error {
|
|
email = strings.TrimSpace(email)
|
|
|
|
if email == "" {
|
|
return ValidationError{Field: "email", Message: "email is required"}
|
|
}
|
|
|
|
if len(email) > 254 {
|
|
return ValidationError{Field: "email", Message: "email must not exceed 254 characters"}
|
|
}
|
|
|
|
if !emailRegex.MatchString(email) {
|
|
return ValidationError{Field: "email", Message: "invalid email format"}
|
|
}
|
|
|
|
parts := strings.Split(email, "@")
|
|
if len(parts[0]) > 64 {
|
|
return ValidationError{Field: "email", Message: "email local part must not exceed 64 characters"}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidatePassword(password string) error {
|
|
if password == "" {
|
|
return ValidationError{Field: "password", Message: "password is required"}
|
|
}
|
|
|
|
if len(password) < 8 {
|
|
return ValidationError{Field: "password", Message: "password must be at least 8 characters long"}
|
|
}
|
|
|
|
if len(password) > 128 {
|
|
return ValidationError{Field: "password", Message: "password must not exceed 128 characters"}
|
|
}
|
|
|
|
var (
|
|
hasUpper bool
|
|
hasLower bool
|
|
hasDigit bool
|
|
hasSpecial bool
|
|
)
|
|
|
|
for _, char := range password {
|
|
switch {
|
|
case unicode.IsUpper(char):
|
|
hasUpper = true
|
|
case unicode.IsLower(char):
|
|
hasLower = true
|
|
case unicode.IsDigit(char):
|
|
hasDigit = true
|
|
case unicode.IsPunct(char) || unicode.IsSymbol(char):
|
|
hasSpecial = true
|
|
}
|
|
}
|
|
|
|
if !hasUpper {
|
|
return ValidationError{Field: "password", Message: "password must contain at least one uppercase letter"}
|
|
}
|
|
|
|
if !hasLower {
|
|
return ValidationError{Field: "password", Message: "password must contain at least one lowercase letter"}
|
|
}
|
|
|
|
if !hasDigit {
|
|
return ValidationError{Field: "password", Message: "password must contain at least one digit"}
|
|
}
|
|
|
|
if !hasSpecial {
|
|
return ValidationError{Field: "password", Message: "password must contain at least one special character"}
|
|
}
|
|
|
|
if IsCommonPassword(password) {
|
|
return ValidationError{Field: "password", Message: "password is too common, please choose a more secure password"}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateTimezone(timezone string) error {
|
|
timezone = strings.TrimSpace(timezone)
|
|
|
|
if timezone == "" {
|
|
return ValidationError{Field: "timezone", Message: "timezone is required"}
|
|
}
|
|
|
|
_, err := time.LoadLocation(timezone)
|
|
if err != nil {
|
|
return ValidationError{Field: "timezone", Message: "invalid timezone, must be a valid IANA timezone (e.g., 'America/New_York', 'Europe/Madrid', 'UTC')"}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateRegistration(email, password, timezone string) error {
|
|
if err := ValidateEmail(email); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ValidatePassword(password); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ValidateTimezone(timezone); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|