diff --git a/app/config/config.go b/app/config/config.go index eee2bf9..1d6f5cc 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -10,12 +10,14 @@ type Configuration struct { Targets []Target `yaml:"targets"` Apis []Api `yaml:"apis"` Routes []Route `yaml:"routes"` + Debug bool `yaml:"debug"` } type Gateway struct { Host string `yaml:"host"` Port int `yaml:"port"` Features []string `yaml:"features"` + Debug bool `yaml:"debug"` } type Host struct { @@ -60,6 +62,12 @@ type Auth struct { Type string `yaml:"type"` Header string `yaml:"header"` Path AuthPath `yaml:"path"` + Keys []ApiKey `yaml:"keys"` +} + +type ApiKey struct { + ID string `yaml:"id"` + Key string `yaml:"key"` } type AuthPath struct { diff --git a/app/go.mod b/app/go.mod index 56b2261..70c1163 100644 --- a/app/go.mod +++ b/app/go.mod @@ -6,6 +6,7 @@ require ( git.secnex.io/secnex/masterlog v0.1.0 github.com/go-chi/chi/v5 v5.2.4 go.yaml.in/yaml/v3 v3.0.4 + golang.org/x/crypto v0.31.0 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.31.1 ) @@ -19,7 +20,7 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/kr/text v0.2.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - golang.org/x/crypto v0.31.0 // indirect golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect ) diff --git a/app/go.sum b/app/go.sum index 312cb3a..48bd499 100644 --- a/app/go.sum +++ b/app/go.sum @@ -37,6 +37,8 @@ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/app/main.go b/app/main.go index d17d8da..5aa1509 100644 --- a/app/main.go +++ b/app/main.go @@ -9,20 +9,26 @@ import ( ) func main() { - masterlog.SetLevel(masterlog.LevelDebug) - masterlog.AddEncoder(&masterlog.JSONEncoder{}) - pseudonymizer := masterlog.NewPseudonymizerFromString("your-secret-key") - masterlog.SetPseudonymizer(pseudonymizer) - masterlog.AddSensitiveFields("user_id", "email", "ip") - - cfg, err := config.NewFile("../gateway.yaml") + cfgFile, err := config.NewFile("../gateway.yaml") if err != nil { masterlog.Error("Failed to load config", map[string]interface{}{ "error": err, }) os.Exit(1) } + cfg := cfgFile.GetConfiguration() + if cfg.Gateway.Debug { + masterlog.Info("Debug mode enabled", map[string]interface{}{}) + masterlog.SetLevel(masterlog.LevelDebug) + } else { + masterlog.Info("Debug mode disabled", map[string]interface{}{}) + masterlog.SetLevel(masterlog.LevelInfo) + } + masterlog.AddEncoder(&masterlog.JSONEncoder{}) + pseudonymizer := masterlog.NewPseudonymizerFromString("your-secret-key") + masterlog.SetPseudonymizer(pseudonymizer) + masterlog.AddSensitiveFields("user_id", "email", "ip") - gateway := server.NewGateway(cfg.GetConfiguration()) + gateway := server.NewGateway(cfg) gateway.Start() } diff --git a/app/middlewares/auth.go b/app/middlewares/auth.go index d76bfef..ace2796 100644 --- a/app/middlewares/auth.go +++ b/app/middlewares/auth.go @@ -1,31 +1,42 @@ package middlewares import ( + "crypto/subtle" + "encoding/base64" + "fmt" "net/http" "strings" "git.secnex.io/secnex/api-gateway/config" + "git.secnex.io/secnex/api-gateway/res" "git.secnex.io/secnex/masterlog" + "golang.org/x/crypto/argon2" ) // AuthMiddleware handles authentication based on header validation and path filtering type AuthMiddleware struct { - header string - pathConfig config.AuthPath - handler http.Handler + header string + authType string + pathConfig config.AuthPath + keys []config.ApiKey + handler http.Handler } // NewAuthMiddleware creates a new authentication middleware -func NewAuthMiddleware(header string, pathConfig config.AuthPath, handler http.Handler) http.Handler { +func NewAuthMiddleware(header string, authType string, pathConfig config.AuthPath, keys []config.ApiKey, handler http.Handler) http.Handler { masterlog.Debug("Creating AuthMiddleware", map[string]interface{}{ - "header": header, + "header": header, + "type": authType, "include_paths": pathConfig.Include, "exclude_paths": pathConfig.Exclude, + "keys_count": len(keys), }) return &AuthMiddleware{ header: header, + authType: authType, pathConfig: pathConfig, + keys: keys, handler: handler, } } @@ -47,7 +58,7 @@ func (m *AuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !requiresAuth { // No auth required, skip to next handler masterlog.Debug("AuthMiddleware: Skipping auth for path", map[string]interface{}{ - "path": requestPath, + "path": requestPath, "reason": "path_matches_exclude_or_not_include", }) m.handler.ServeHTTP(w, r) @@ -57,24 +68,73 @@ func (m *AuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Step 2: Check if auth header is present authHeader := r.Header.Get(m.header) - masterlog.Debug("AuthMiddleware: Checking auth header", map[string]interface{}{ - "path": requestPath, - "header_name": m.header, - "header_present": authHeader != "", - }) - if authHeader == "" { - masterlog.Warn("AuthMiddleware: Missing auth header", map[string]interface{}{ + masterlog.Debug("AuthMiddleware: Missing auth header", map[string]interface{}{ "path": requestPath, "header": m.header, }) - http.Error(w, "Unauthorized: Missing authentication header", http.StatusUnauthorized) + res.Unauthorized(w) return } - // Step 3: Auth header present, remove it before forwarding - // (don't send the auth header to the backend) - r.Header.Del(m.header) + switch m.authType { + case "api_key": + masterlog.Debug("AuthMiddleware: API key authentication", map[string]interface{}{ + "path": requestPath, + "header": m.header, + }) + apiKey := r.Header.Get(m.header) + plainSecret, err := base64.StdEncoding.DecodeString(apiKey) + if err != nil { + masterlog.Debug("AuthMiddleware: API key authentication failed", map[string]interface{}{ + "path": requestPath, + "header": m.header, + "error": err, + }) + res.Unauthorized(w) + return + } + secret := strings.Split(string(plainSecret), ":") + if len(secret) != 2 { + masterlog.Debug("AuthMiddleware: API key authentication failed", map[string]interface{}{ + "path": requestPath, + "header": m.header, + "error": "invalid_api_key_format", + }) + res.Unauthorized(w) + return + } + + // Find matching key by ID + for _, key := range m.keys { + if key.ID == secret[0] { + // Verify argon2 hash + if m.verifyArgon2Hash(secret[1], key.Key) { + masterlog.Debug("AuthMiddleware: API key authentication successful", map[string]interface{}{ + "path": requestPath, + "key_id": key.ID, + }) + m.handler.ServeHTTP(w, r) + return + } + } + } + + masterlog.Debug("AuthMiddleware: API key authentication failed", map[string]interface{}{ + "path": requestPath, + "header": m.header, + "error": "invalid_api_key", + }) + res.Unauthorized(w) + return + case "bearer_token": + masterlog.Debug("AuthMiddleware: Bearer token authentication", map[string]interface{}{ + "path": requestPath, + "header": m.header, + }) + } + + r.Header.Set(m.header, "valid_api_key") masterlog.Debug("AuthMiddleware: Authentication successful", map[string]interface{}{ "path": requestPath, @@ -196,3 +256,57 @@ func (m *AuthMiddleware) matchPattern(path, pattern string) bool { }) return matches } + +// verifyArgon2Hash verifies a password against an argon2id hash +func (m *AuthMiddleware) verifyArgon2Hash(password, hash string) bool { + // Parse the hash format: $argon2id$v=19$m=65536,t=3,p=4$salt$encodedHash + parts := strings.Split(hash, "$") + if len(parts) != 6 { + masterlog.Debug("AuthMiddleware: Invalid hash format", map[string]interface{}{ + "parts_count": len(parts), + }) + return false + } + + if parts[1] != "argon2id" { + masterlog.Debug("AuthMiddleware: Unsupported hash type", map[string]interface{}{ + "type": parts[1], + }) + return false + } + + // Parse parameters + var version int + var memory, time, parallelism uint32 + if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil { + return false + } + if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, ¶llelism); err != nil { + return false + } + + // Decode salt + salt, err := base64.RawStdEncoding.DecodeString(parts[4]) + if err != nil { + masterlog.Debug("AuthMiddleware: Failed to decode salt", map[string]interface{}{ + "error": err, + }) + return false + } + + // Decode stored hash + decodedHash, err := base64.RawStdEncoding.DecodeString(parts[5]) + if err != nil { + masterlog.Debug("AuthMiddleware: Failed to decode hash", map[string]interface{}{ + "error": err, + }) + return false + } + + // Generate hash for comparison + hashLength := uint32(len(decodedHash)) + comparisonHash := argon2.IDKey([]byte(password), salt, time, memory, uint8(parallelism), hashLength) + + // Use constant time comparison to prevent timing attacks + return subtle.ConstantTimeCompare(comparisonHash, decodedHash) == 1 +} diff --git a/app/server/routes.go b/app/server/routes.go index 773675b..2ef15e7 100644 --- a/app/server/routes.go +++ b/app/server/routes.go @@ -39,13 +39,13 @@ func NewRoutes(cfg *config.Configuration, apis Apis) *Routes { func (rs *Routes) Register(r *chi.Mux) { for _, route := range rs.routes { masterlog.Info("Registering route", map[string]interface{}{ - "id": route.ID, - "path": route.Path, - "api": route.Api.ID, - "auth_enabled": route.Security.Auth.Enabled, - "auth_header": route.Security.Auth.Header, - "auth_include": route.Security.Auth.Path.Include, - "auth_exclude": route.Security.Auth.Path.Exclude, + "id": route.ID, + "path": route.Path, + "api": route.Api.ID, + "auth_enabled": route.Security.Auth.Enabled, + "auth_header": route.Security.Auth.Header, + "auth_include": route.Security.Auth.Path.Include, + "auth_exclude": route.Security.Auth.Path.Exclude, }) handler := route.createHandler() @@ -66,7 +66,9 @@ func (r *Route) createHandler() http.Handler { }) handler = middlewares.NewAuthMiddleware( r.Security.Auth.Header, + r.Security.Auth.Type, r.Security.Auth.Path, + r.Security.Auth.Keys, handler, ) }