Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion platform-api/src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
"os"
"reflect"
"strings"
"sync"
Expand Down Expand Up @@ -295,12 +296,19 @@ func LoadConfig(configPath string) (*Server, error) {
}

if cfg.Auth.JWT.Enabled && cfg.Auth.JWT.SecretKey == "" {
if !demoMode() {
return nil, fmt.Errorf(
"AUTH_JWT_SECRET_KEY must be configured when APIP_DEMO_MODE=false and JWT authentication is enabled; " +
"generate a secret with: openssl rand -hex 32",
)
}
key, err := generateRandomSecret()
if err != nil {
return nil, fmt.Errorf("failed to generate JWT secret key: %w", err)
}
cfg.Auth.JWT.SecretKey = key
slog.Warn("auth.jwt.secret_key is not set — generated an ephemeral random key; all sessions will be invalidated on restart")
slog.Warn("JWT_SIGNING_SECRET not set — generated an ephemeral demo key (restart will invalidate all sessions)",
slog.String("JWT_SIGNING_SECRET", key))
}

return cfg, nil
Expand All @@ -314,6 +322,16 @@ func generateRandomSecret() (string, error) {
return hex.EncodeToString(b), nil
}

// demoMode reports whether APIP_DEMO_MODE is enabled.
// Defaults to true when the variable is unset.
func demoMode() bool {
v := strings.ToLower(strings.TrimSpace(os.Getenv("APIP_DEMO_MODE")))
if v == "" {
return true
}
return v == "true" || v == "1"
}


// envToKoanfKey maps a lowercased environment variable name to its koanf dot-notation key.
// Returns "" for unknown variables, which causes koanf to skip them.
Expand Down
30 changes: 30 additions & 0 deletions platform-api/src/internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,30 @@ type Server struct {
logger *slog.Logger
}

// validateAuthConfig enforces production auth requirements when demo mode is off.
func validateAuthConfig(cfg *config.Server) error {
if demoMode() {
return nil
}
if cfg.Auth.FileBased.Enabled {
return fmt.Errorf("file-based authentication (AUTH_FILE_BASED_ENABLED=true) is not allowed when APIP_DEMO_MODE=false; configure an IDP (AUTH_IDP_ENABLED=true) or JWT (AUTH_JWT_ENABLED=true) instead")
}
if !cfg.Auth.IDP.Enabled && !cfg.Auth.JWT.Enabled {
return fmt.Errorf("APIP_DEMO_MODE=false requires a real auth mode; set AUTH_IDP_ENABLED=true or AUTH_JWT_ENABLED=true")
}
if cfg.Auth.JWT.Enabled && cfg.Auth.JWT.SkipValidation {
return fmt.Errorf("JWT signature validation cannot be skipped (AUTH_JWT_SKIP_VALIDATION=true) when APIP_DEMO_MODE=false; set AUTH_JWT_SKIP_VALIDATION=false for production")
}
return nil
}

// StartPlatformAPIServer creates a new server instance with all dependencies initialized
func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, error) {
if err := validateAuthConfig(cfg); err != nil {
slogger.Error("Invalid auth configuration for production mode", "error", err)
return nil, err
}

// Initialize database using configuration
db, err := database.NewConnection(&cfg.Database, slogger)
if err != nil {
Expand Down Expand Up @@ -631,6 +653,14 @@ func (s *Server) Start(port string, certDir string) error {

// Generate new certificate if not loaded
if cert.Certificate == nil {
if !demoMode() {
return fmt.Errorf(
"no TLS certificates found at %q (cert.pem / key.pem) and APIP_DEMO_MODE=false: "+
"mount real certificates or set TLS_CERT_DIR to a directory containing cert.pem and key.pem; "+
"self-signed certificate generation is only permitted in demo mode",
certDir,
)
}
s.logger.Info("Generating self-signed certificate for development...")
// Ensure cert directory exists
if err := os.MkdirAll(certDir, 0755); err != nil {
Expand Down
Loading