Files
api-gateway/app/middlewares/auth.go
2026-02-06 00:08:27 +01:00

199 lines
5.9 KiB
Go

package middlewares
import (
"net/http"
"strings"
"git.secnex.io/secnex/api-gateway/config"
"git.secnex.io/secnex/masterlog"
)
// AuthMiddleware handles authentication based on header validation and path filtering
type AuthMiddleware struct {
header string
pathConfig config.AuthPath
handler http.Handler
}
// NewAuthMiddleware creates a new authentication middleware
func NewAuthMiddleware(header string, pathConfig config.AuthPath, handler http.Handler) http.Handler {
masterlog.Debug("Creating AuthMiddleware", map[string]interface{}{
"header": header,
"include_paths": pathConfig.Include,
"exclude_paths": pathConfig.Exclude,
})
return &AuthMiddleware{
header: header,
pathConfig: pathConfig,
handler: handler,
}
}
// ServeHTTP handles the authentication logic
func (m *AuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
requestPath := r.URL.Path
// Step 1: Determine if this path requires authentication
requiresAuth := m.requiresAuth(requestPath)
masterlog.Debug("AuthMiddleware: Checking if path requires auth", map[string]interface{}{
"path": requestPath,
"requires_auth": requiresAuth,
"include": m.pathConfig.Include,
"exclude": m.pathConfig.Exclude,
})
if !requiresAuth {
// No auth required, skip to next handler
masterlog.Debug("AuthMiddleware: Skipping auth for path", map[string]interface{}{
"path": requestPath,
"reason": "path_matches_exclude_or_not_include",
})
m.handler.ServeHTTP(w, r)
return
}
// 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{}{
"path": requestPath,
"header": m.header,
})
http.Error(w, "Unauthorized: Missing authentication header", http.StatusUnauthorized)
return
}
// Step 3: Auth header present, remove it before forwarding
// (don't send the auth header to the backend)
r.Header.Del(m.header)
masterlog.Debug("AuthMiddleware: Authentication successful", map[string]interface{}{
"path": requestPath,
})
// Step 4: Forward to next handler
m.handler.ServeHTTP(w, r)
}
// requiresAuth determines if a given path requires authentication
//
// Logic:
// 1. If BOTH include and exclude are empty → auth required for ALL paths
// 2. If ONLY include is set (non-empty) → auth required ONLY for paths matching include patterns
// 3. If ONLY exclude is set (non-empty) → auth required for ALL paths EXCEPT those matching exclude patterns
// 4. If BOTH are set → include takes precedence (auth required ONLY for paths matching include patterns)
//
// Wildcard patterns are supported:
// - "*" matches any path
// - "/api/*" matches "/api/" and any subpath like "/api/users", "/api/users/123"
// - "/api/v1/public/test/*" matches "/test", "/test/123", etc.
func (m *AuthMiddleware) requiresAuth(path string) bool {
include := m.pathConfig.Include
exclude := m.pathConfig.Exclude
includeEmpty := len(include) == 0
excludeEmpty := len(exclude) == 0
masterlog.Debug("AuthMiddleware: Evaluating auth requirement", map[string]interface{}{
"path": path,
"include_empty": includeEmpty,
"exclude_empty": excludeEmpty,
"include": include,
"exclude": exclude,
})
// Case 1: Both include and exclude are empty → auth required for ALL
if includeEmpty && excludeEmpty {
masterlog.Debug("AuthMiddleware: Both include/exclude empty, auth required for all", map[string]interface{}{
"path": path,
})
return true
}
// Case 2: Only include is set → auth required ONLY for matching paths
if !includeEmpty {
for _, pattern := range include {
if m.matchPattern(path, pattern) {
masterlog.Debug("AuthMiddleware: Path matches include pattern", map[string]interface{}{
"path": path,
"pattern": pattern,
})
return true
}
}
masterlog.Debug("AuthMiddleware: Path does not match any include pattern", map[string]interface{}{
"path": path,
"patterns": include,
})
return false
}
// Case 3: Only exclude is set (include is empty) → auth required EXCEPT for matching paths
// This is also reached when both are set (include takes precedence above)
for _, pattern := range exclude {
if m.matchPattern(path, pattern) {
masterlog.Debug("AuthMiddleware: Path matches exclude pattern", map[string]interface{}{
"path": path,
"pattern": pattern,
})
return false
}
}
masterlog.Debug("AuthMiddleware: Path does not match any exclude pattern, auth required", map[string]interface{}{
"path": path,
"patterns": exclude,
})
return true
}
// matchPattern checks if a path matches a wildcard pattern
//
// Supported patterns:
// - "*" matches any path
// - "/api/*" matches "/api/" and any subpath
// - "/api/v1/public/test/*" matches the exact prefix and any subpath
//
// The pattern matching is prefix-based. If the pattern ends with "*",
// it matches any path that starts with the pattern (excluding the "*").
func (m *AuthMiddleware) matchPattern(path, pattern string) bool {
// Wildcard: matches everything
if pattern == "*" {
masterlog.Debug("AuthMiddleware: Wildcard pattern matches", map[string]interface{}{
"path": path,
})
return true
}
// Pattern ends with wildcard: prefix matching
if strings.HasSuffix(pattern, "*") {
prefix := strings.TrimSuffix(pattern, "*")
matches := strings.HasPrefix(path, prefix)
masterlog.Debug("AuthMiddleware: Prefix pattern matching", map[string]interface{}{
"path": path,
"pattern": pattern,
"prefix": prefix,
"matches": matches,
})
return matches
}
// Exact match
matches := path == pattern
masterlog.Debug("AuthMiddleware: Exact pattern matching", map[string]interface{}{
"path": path,
"pattern": pattern,
"matches": matches,
})
return matches
}