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"`
|
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 {
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
22
app/main.go
22
app/main.go
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, ¶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) {
|
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user