feat(auth): Add api key authentication to config and add validation with argon2
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
22
app/main.go
22
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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user