// Package security provides security-related middleware for the Lolly HTTP server. // // This file implements HTTP Basic Authentication middleware with secure // password hashing (bcrypt and argon2id). It enforces HTTPS by default. // // Example usage: // // cfg := &config.AuthConfig{ // Type: "basic", // RequireTLS: true, // Algorithm: "bcrypt", // Users: []config.User{ // {Name: "admin", Password: "$2b$12$..."}, // bcrypt hash // }, // Realm: "Restricted Area", // } // // auth, err := security.NewBasicAuth(cfg) // if err != nil { // log.Fatal(err) // } // // // Apply as middleware // chain := middleware.NewChain(auth) // handler := chain.Apply(finalHandler) // //go:generate go test -v ./... package security import ( "encoding/base64" "errors" "fmt" "strings" "sync" "github.com/valyala/fasthttp" "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/argon2" "rua.plus/lolly/internal/config" "rua.plus/lolly/internal/middleware" ) // HashAlgorithm represents the password hashing algorithm type. type HashAlgorithm int const ( HashBcrypt HashAlgorithm = iota // bcrypt (default, recommended) HashArgon2id // Argon2id (more secure, compute-intensive) ) // BasicAuth implements HTTP Basic Authentication middleware. type BasicAuth struct { users map[string]string // username -> hashed password algorithm HashAlgorithm // Hash algorithm used realm string // Authentication realm requireTLS bool // Require HTTPS (default true) minPasswordLength int // Minimum password length for validation argon2Params argon2Params // Argon2id parameters mu sync.RWMutex } // argon2Params holds Argon2id configuration parameters. type argon2Params struct { time uint32 // Number of passes memory uint32 // Memory cost in KB threads uint8 // Parallelism saltLen uint32 // Salt length keyLen uint32 // Output key length } // Default Argon2id parameters (OWASP recommended) var defaultArgon2Params = argon2Params{ time: 3, memory: 64 * 1024, // 64 MB threads: 4, saltLen: 16, keyLen: 32, } // NewBasicAuth creates a new Basic Auth middleware from configuration. // // Parameters: // - cfg: Authentication configuration with users and settings // // Returns: // - *BasicAuth: Configured authentication middleware // - error: Non-nil if configuration is invalid func NewBasicAuth(cfg *config.AuthConfig) (*BasicAuth, error) { if cfg == nil { return nil, errors.New("auth config is nil") } if cfg.Type != "basic" { return nil, fmt.Errorf("unsupported auth type: %s", cfg.Type) } if len(cfg.Users) == 0 { return nil, errors.New("no users configured") } auth := &BasicAuth{ users: make(map[string]string), requireTLS: cfg.RequireTLS, // Default is true from config defaults minPasswordLength: cfg.MinPasswordLength, argon2Params: defaultArgon2Params, } // Set realm if cfg.Realm != "" { auth.realm = cfg.Realm } else { auth.realm = "Restricted Area" } // Set hash algorithm switch strings.ToLower(cfg.Algorithm) { case "bcrypt", "": auth.algorithm = HashBcrypt case "argon2id": auth.algorithm = HashArgon2id default: return nil, fmt.Errorf("unsupported hash algorithm: %s", cfg.Algorithm) } // Load users for _, user := range cfg.Users { if user.Name == "" { return nil, errors.New("username cannot be empty") } if user.Password == "" { return nil, fmt.Errorf("password for user %s cannot be empty", user.Name) } // Validate password hash format if err := validatePasswordHash(user.Password, auth.algorithm); err != nil { return nil, fmt.Errorf("invalid password hash for user %s: %w", user.Name, err) } auth.users[user.Name] = user.Password } return auth, nil } // Name returns the middleware name. func (ba *BasicAuth) Name() string { return "basic_auth" } // Process wraps the next handler with authentication logic. // Returns 401 Unauthorized if authentication fails. func (ba *BasicAuth) Process(next fasthttp.RequestHandler) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { // Check TLS requirement if ba.requireTLS && !ctx.IsTLS() { ctx.Error("Forbidden: HTTPS required for authentication", fasthttp.StatusForbidden) return } // Extract and validate credentials username, password, ok := ba.extractCredentials(ctx) if !ok { ba.sendAuthChallenge(ctx) return } // Authenticate if !ba.Authenticate(username, password) { ba.sendAuthChallenge(ctx) return } // Success - proceed to next handler next(ctx) } } // Authenticate validates username and password credentials. // Returns true if authentication succeeds. func (ba *BasicAuth) Authenticate(username, password string) bool { ba.mu.RLock() hashedPassword, exists := ba.users[username] ba.mu.RUnlock() if !exists { return false } switch ba.algorithm { case HashBcrypt: return authenticateBcrypt(password, hashedPassword) case HashArgon2id: return authenticateArgon2id(password, hashedPassword) default: return false } } // authenticateBcrypt verifies password against bcrypt hash. func authenticateBcrypt(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } // authenticateArgon2id verifies password against Argon2id hash. // Hash format: $argon2id$v=19$m=,t=