feat(auth): Add api key authentication to config and add validation with argon2

This commit is contained in:
Björn Benouarets
2026-02-09 07:39:44 +01:00
parent 78da787f43
commit 9f3177bf5b
6 changed files with 166 additions and 33 deletions

View File

@@ -10,12 +10,14 @@ type Configuration struct {
Targets []Target `yaml:"targets"` Targets []Target `yaml:"targets"`
Apis []Api `yaml:"apis"` Apis []Api `yaml:"apis"`
Routes []Route `yaml:"routes"` Routes []Route `yaml:"routes"`
Debug bool `yaml:"debug"`
} }
type Gateway struct { type Gateway struct {
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
Features []string `yaml:"features"` Features []string `yaml:"features"`
Debug bool `yaml:"debug"`
} }
type Host struct { type Host struct {
@@ -60,6 +62,12 @@ type Auth struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Header string `yaml:"header"` Header string `yaml:"header"`
Path AuthPath `yaml:"path"` Path AuthPath `yaml:"path"`
Keys []ApiKey `yaml:"keys"`
}
type ApiKey struct {
ID string `yaml:"id"`
Key string `yaml:"key"`
} }
type AuthPath struct { type AuthPath struct {

View File

@@ -6,6 +6,7 @@ require (
git.secnex.io/secnex/masterlog v0.1.0 git.secnex.io/secnex/masterlog v0.1.0
github.com/go-chi/chi/v5 v5.2.4 github.com/go-chi/chi/v5 v5.2.4
go.yaml.in/yaml/v3 v3.0.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/driver/postgres v1.6.0
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
) )
@@ -19,7 +20,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // 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/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.21.0 // indirect
) )

View File

@@ -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/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 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -9,20 +9,26 @@ import (
) )
func main() { func main() {
masterlog.SetLevel(masterlog.LevelDebug) cfgFile, err := config.NewFile("../gateway.yaml")
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")
if err != nil { if err != nil {
masterlog.Error("Failed to load config", map[string]interface{}{ masterlog.Error("Failed to load config", map[string]interface{}{
"error": err, "error": err,
}) })
os.Exit(1) 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() gateway.Start()
} }

View File

@@ -1,31 +1,42 @@
package middlewares package middlewares
import ( import (
"crypto/subtle"
"encoding/base64"
"fmt"
"net/http" "net/http"
"strings" "strings"
"git.secnex.io/secnex/api-gateway/config" "git.secnex.io/secnex/api-gateway/config"
"git.secnex.io/secnex/api-gateway/res"
"git.secnex.io/secnex/masterlog" "git.secnex.io/secnex/masterlog"
"golang.org/x/crypto/argon2"
) )
// AuthMiddleware handles authentication based on header validation and path filtering // AuthMiddleware handles authentication based on header validation and path filtering
type AuthMiddleware struct { type AuthMiddleware struct {
header string header string
pathConfig config.AuthPath authType string
handler http.Handler pathConfig config.AuthPath
keys []config.ApiKey
handler http.Handler
} }
// NewAuthMiddleware creates a new authentication middleware // 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{}{ masterlog.Debug("Creating AuthMiddleware", map[string]interface{}{
"header": header, "header": header,
"type": authType,
"include_paths": pathConfig.Include, "include_paths": pathConfig.Include,
"exclude_paths": pathConfig.Exclude, "exclude_paths": pathConfig.Exclude,
"keys_count": len(keys),
}) })
return &AuthMiddleware{ return &AuthMiddleware{
header: header, header: header,
authType: authType,
pathConfig: pathConfig, pathConfig: pathConfig,
keys: keys,
handler: handler, handler: handler,
} }
} }
@@ -47,7 +58,7 @@ func (m *AuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !requiresAuth { if !requiresAuth {
// No auth required, skip to next handler // No auth required, skip to next handler
masterlog.Debug("AuthMiddleware: Skipping auth for path", map[string]interface{}{ masterlog.Debug("AuthMiddleware: Skipping auth for path", map[string]interface{}{
"path": requestPath, "path": requestPath,
"reason": "path_matches_exclude_or_not_include", "reason": "path_matches_exclude_or_not_include",
}) })
m.handler.ServeHTTP(w, r) 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 // Step 2: Check if auth header is present
authHeader := r.Header.Get(m.header) 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 == "" { if authHeader == "" {
masterlog.Warn("AuthMiddleware: Missing auth header", map[string]interface{}{ masterlog.Debug("AuthMiddleware: Missing auth header", map[string]interface{}{
"path": requestPath, "path": requestPath,
"header": m.header, "header": m.header,
}) })
http.Error(w, "Unauthorized: Missing authentication header", http.StatusUnauthorized) res.Unauthorized(w)
return return
} }
// Step 3: Auth header present, remove it before forwarding switch m.authType {
// (don't send the auth header to the backend) case "api_key":
r.Header.Del(m.header) 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{}{ masterlog.Debug("AuthMiddleware: Authentication successful", map[string]interface{}{
"path": requestPath, "path": requestPath,
@@ -196,3 +256,57 @@ func (m *AuthMiddleware) matchPattern(path, pattern string) bool {
}) })
return matches 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, &parallelism); 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
}

View File

@@ -39,13 +39,13 @@ func NewRoutes(cfg *config.Configuration, apis Apis) *Routes {
func (rs *Routes) Register(r *chi.Mux) { func (rs *Routes) Register(r *chi.Mux) {
for _, route := range rs.routes { for _, route := range rs.routes {
masterlog.Info("Registering route", map[string]interface{}{ masterlog.Info("Registering route", map[string]interface{}{
"id": route.ID, "id": route.ID,
"path": route.Path, "path": route.Path,
"api": route.Api.ID, "api": route.Api.ID,
"auth_enabled": route.Security.Auth.Enabled, "auth_enabled": route.Security.Auth.Enabled,
"auth_header": route.Security.Auth.Header, "auth_header": route.Security.Auth.Header,
"auth_include": route.Security.Auth.Path.Include, "auth_include": route.Security.Auth.Path.Include,
"auth_exclude": route.Security.Auth.Path.Exclude, "auth_exclude": route.Security.Auth.Path.Exclude,
}) })
handler := route.createHandler() handler := route.createHandler()
@@ -66,7 +66,9 @@ func (r *Route) createHandler() http.Handler {
}) })
handler = middlewares.NewAuthMiddleware( handler = middlewares.NewAuthMiddleware(
r.Security.Auth.Header, r.Security.Auth.Header,
r.Security.Auth.Type,
r.Security.Auth.Path, r.Security.Auth.Path,
r.Security.Auth.Keys,
handler, handler,
) )
} }