feat(auth): Add authentication middleware
This commit is contained in:
198
app/middlewares/auth.go
Normal file
198
app/middlewares/auth.go
Normal file
@@ -0,0 +1,198 @@
|
||||
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
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.secnex.io/secnex/api-gateway/config"
|
||||
"git.secnex.io/secnex/api-gateway/middlewares"
|
||||
"git.secnex.io/secnex/masterlog"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
@@ -17,6 +18,7 @@ type Route struct {
|
||||
ID string
|
||||
Path string
|
||||
StripPrefix config.StripPrefix
|
||||
Security config.Security
|
||||
Api *Api
|
||||
}
|
||||
|
||||
@@ -27,6 +29,7 @@ func NewRoutes(cfg *config.Configuration, apis Apis) *Routes {
|
||||
ID: route.ID,
|
||||
Path: route.Path,
|
||||
StripPrefix: route.StripPrefix,
|
||||
Security: route.Security,
|
||||
Api: apis[route.Api],
|
||||
})
|
||||
}
|
||||
@@ -36,9 +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,
|
||||
"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()
|
||||
@@ -47,9 +54,29 @@ func (rs *Routes) Register(r *chi.Mux) {
|
||||
}
|
||||
|
||||
func (r *Route) createHandler() http.Handler {
|
||||
// Start with the API (proxy) handler
|
||||
handler := http.Handler(r.Api)
|
||||
|
||||
// Apply middlewares in reverse order (last one wraps first)
|
||||
// 1. Auth middleware (if enabled)
|
||||
if r.Security.Auth.Enabled {
|
||||
masterlog.Debug("Route: Applying Auth middleware", map[string]interface{}{
|
||||
"path": r.Path,
|
||||
"header": r.Security.Auth.Header,
|
||||
})
|
||||
handler = middlewares.NewAuthMiddleware(
|
||||
r.Security.Auth.Header,
|
||||
r.Security.Auth.Path,
|
||||
handler,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Strip prefix middleware (if enabled)
|
||||
if r.StripPrefix.Enabled {
|
||||
masterlog.Debug("Route: Applying StripPrefix middleware", map[string]interface{}{
|
||||
"path": r.Path,
|
||||
"prefix": r.StripPrefix.Prefix,
|
||||
})
|
||||
handler = newStripPrefixMiddleware(r.StripPrefix.Prefix, handler)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user