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

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