diff --git a/cmd/server/main_test.go b/cmd/server/main_test.go index d204e54..4e5d425 100644 --- a/cmd/server/main_test.go +++ b/cmd/server/main_test.go @@ -431,7 +431,7 @@ func TestConfigurationValidation(t *testing.T) { }() // Test configuration loading - _, err := config.LoadConfig("") + _, err := config.LoadConfig(t.TempDir()) if tt.shouldError { if err == nil { t.Errorf("Expected error but got none") @@ -548,7 +548,7 @@ func TestTLSConfiguration(t *testing.T) { }() // Load configuration - cfg, err := config.LoadConfig("") + cfg, err := config.LoadConfig(t.TempDir()) if err != nil { t.Fatalf("Failed to load config: %v", err) } @@ -662,7 +662,7 @@ func TestMainFunctionIntegration(t *testing.T) { }() // Test that configuration loads successfully - cfg, err := config.LoadConfig("") + cfg, err := config.LoadConfig(t.TempDir()) if err != nil { t.Fatalf("Configuration should load successfully: %v", err) } diff --git a/config/config.go b/config/config.go index e19e809..7e51b31 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,8 @@ package config import ( + "crypto/rand" + "encoding/hex" "fmt" "os" "path/filepath" @@ -138,7 +140,11 @@ func (c *Config) loadFromEnv() error { if jwtSecret := os.Getenv("MARCHAT_JWT_SECRET"); jwtSecret != "" { c.JWTSecret = jwtSecret } else { - c.JWTSecret = "marchat-default-secret-change-in-production" + jwtSecret, err := GenerateJWTSecret() + if err != nil { + return fmt.Errorf("when generating JWT secret: %w", err) + } + c.JWTSecret = jwtSecret } // TLS configuration @@ -302,3 +308,12 @@ func (c *Config) GetWebSocketScheme() string { } return "ws" } + +func GenerateJWTSecret() (string, error) { + jwtSecret := make([]byte, 32) + _, err := rand.Read(jwtSecret) + if err != nil { + return "", fmt.Errorf("when reading random bytes: %w", err) + } + return hex.EncodeToString(jwtSecret), nil +} diff --git a/config/config_test.go b/config/config_test.go index 556804a..2c92ff0 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "encoding/hex" "os" "path/filepath" "reflect" @@ -21,7 +22,7 @@ func TestLoadConfig(t *testing.T) { os.Unsetenv("MARCHAT_USERS") }() - cfg, err := LoadConfig("") + cfg, err := LoadConfig(t.TempDir()) if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -47,7 +48,7 @@ func TestLoadConfig(t *testing.T) { os.Unsetenv("MARCHAT_ADMIN_KEY") os.Unsetenv("MARCHAT_USERS") - _, err := LoadConfig("") + _, err := LoadConfig(t.TempDir()) if err == nil { t.Error("Expected error when required environment variables are missing") } @@ -63,7 +64,7 @@ func TestLoadConfig(t *testing.T) { os.Unsetenv("MARCHAT_USERS") }() - _, err := LoadConfig("") + _, err := LoadConfig(t.TempDir()) if err == nil { t.Error("Expected error for invalid port") } @@ -347,3 +348,35 @@ func TestGetEnvIntWithDefault(t *testing.T) { t.Errorf("Expected 789 (default), got %d", result) } } + +// TestJWTSecretGeneratedRandomly checks that JWTSecret is a 64 char long generated hex when not provided +func TestJWTSecretGeneratedRandomly(t *testing.T) { + os.Setenv("MARCHAT_PORT", "8080") + os.Setenv("MARCHAT_ADMIN_KEY", "test-key") + os.Setenv("MARCHAT_USERS", "user1,user2") + os.Unsetenv("MARCHAT_JWT_SECRET") + defer func() { + os.Unsetenv("MARCHAT_PORT") + os.Unsetenv("MARCHAT_ADMIN_KEY") + os.Unsetenv("MARCHAT_USERS") + }() + + cfg, err := LoadConfig(t.TempDir()) + if err != nil { + t.Fatalf("LoadConfig failed: %v", err) + } + + if len(cfg.JWTSecret) != 64 { + t.Fatalf("invalid JWTSecret length: %s (%d)", cfg.JWTSecret, len(cfg.JWTSecret)) + } + + dest := make([]byte, 32) + n, err := hex.Decode(dest, []byte(cfg.JWTSecret)) + if err != nil { + t.Fatalf("JWT Secret is not made of hex: %s", err.Error()) + } + if n != 32 { + t.Errorf("unexpected number of bytes decoded: %d", n) + } + +} diff --git a/env.example b/env.example index 14d2fcc..a4bd408 100644 --- a/env.example +++ b/env.example @@ -1,7 +1,7 @@ # ============================================================================= # marchat Server Environment Configuration # ============================================================================= -# +# # Copy this file to .env and customize the values for your deployment: # cp env.example .env # @@ -52,10 +52,9 @@ MARCHAT_LOG_LEVEL=info # JWT Configuration (Future Use) # ============================================================================= -# JWT secret for authentication (auto-generated if not set) +# JWT secret for authentication (auto-generated randomly if not set) # This will be used for enhanced authentication features in future releases -# IMPORTANT: Change this to a secure value in production! -MARCHAT_JWT_SECRET=your-jwt-secret-change-in-production +MARCHAT_JWT_SECRET=your-jwt-secret # ============================================================================= # Advanced Configuration (Optional) @@ -91,4 +90,4 @@ MARCHAT_JWT_SECRET=your-jwt-secret-change-in-production # - Setting MARCHAT_LOG_LEVEL=warn or error # - Using absolute paths for MARCHAT_DB_PATH # - Changing all default secrets to secure values -# ============================================================================= \ No newline at end of file +# ============================================================================= diff --git a/server/config_ui.go b/server/config_ui.go index 5728c50..37e64cd 100644 --- a/server/config_ui.go +++ b/server/config_ui.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/Cod-e-Codes/marchat/config" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -49,6 +50,7 @@ type ServerConfig struct { AdminKey string AdminUsers string Port string + JWTSecret string } func NewServerConfigUI() ServerConfigModel { @@ -138,6 +140,7 @@ func (m *ServerConfigModel) saveConfigToEnv() error { envContent.WriteString(fmt.Sprintf("MARCHAT_PORT=%s\n", m.config.Port)) envContent.WriteString(fmt.Sprintf("MARCHAT_ADMIN_KEY=%s\n", m.config.AdminKey)) envContent.WriteString(fmt.Sprintf("MARCHAT_USERS=%s\n", m.config.AdminUsers)) + envContent.WriteString(fmt.Sprintf("MARCHAT_JWT_SECRET=%s\n", m.config.JWTSecret)) // Write to file if err := os.WriteFile(envPath, []byte(envContent.String()), 0600); err != nil { @@ -288,11 +291,17 @@ func (m *ServerConfigModel) validateAndBuildConfig() error { return fmt.Errorf("port must be a number between 1 and 65535") } + jwtSecret, err := config.GenerateJWTSecret() + if err != nil { + return fmt.Errorf("failed at generating JWT secret: %w", err) + } + // Build config m.config = &ServerConfig{ AdminKey: adminKey, AdminUsers: adminUsers, Port: port, + JWTSecret: jwtSecret, } // Save configuration to .env file diff --git a/server/config_ui_test.go b/server/config_ui_test.go index 3238999..da97534 100644 --- a/server/config_ui_test.go +++ b/server/config_ui_test.go @@ -31,7 +31,14 @@ func TestServerConfigUISavesEnv(t *testing.T) { // Basic content checks b, _ := os.ReadFile(envPath) content := string(b) - if !strings.Contains(content, "MARCHAT_PORT=8123") || !strings.Contains(content, "MARCHAT_ADMIN_KEY=adminkey123") || !strings.Contains(content, "MARCHAT_USERS=alice,bob") { - t.Fatalf("unexpected .env content: %s", content) + for _, needle := range []string{ + "MARCHAT_PORT=8123", + "MARCHAT_ADMIN_KEY=adminkey123", + "MARCHAT_USERS=alice,bob", + "MARCHAT_JWT_SECRET=", // The value is random, so we only check for the presence of the env var + } { + if !strings.Contains(content, needle) { + t.Fatalf("missing .env content: %s", needle) + } } }