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"`
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 {

View File

@@ -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
)

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/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=

View File

@@ -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()
}

View File

@@ -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, &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) {
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,
)
}