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 }