From 78da787f439da495ac6072838b4c3b95d986493e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Benouarets?= Date: Fri, 6 Feb 2026 00:08:27 +0100 Subject: [PATCH] feat(auth): Add authentication middleware --- .docs/architecture.md | 98 ++++++++++++++-- .docs/configuration.md | 224 +++++++++++++++++++++++++------------ .docs/usage.md | 240 ++++++++++++++++++++++++++++++++++------ app/middlewares/auth.go | 198 +++++++++++++++++++++++++++++++++ app/server/routes.go | 33 +++++- 5 files changed, 672 insertions(+), 121 deletions(-) create mode 100644 app/middlewares/auth.go diff --git a/.docs/architecture.md b/.docs/architecture.md index 12d476b..1f4f873 100644 --- a/.docs/architecture.md +++ b/.docs/architecture.md @@ -25,10 +25,10 @@ The SecNex API Gateway follows a modular architecture with clear separation of c │ Route Handler │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Per-Route Middleware Chain │ │ -│ │ ┌────────────┐ ┌──────────────┐ │ │ -│ │ │ Strip │→ │ Reverse │ │ │ -│ │ │ Prefix │ │ Proxy │ │ │ -│ │ └────────────┘ └──────────────┘ │ │ +│ │ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │ │ +│ │ │ Auth │→ │ Strip │→ │ Reverse │ │ │ +│ │ │ Middleware │ │ Prefix │ │ Proxy │ │ │ +│ │ └────────────┘ └────────────┘ └──────────────┘ │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────┬──────────────────────────────────┘ │ @@ -54,11 +54,12 @@ app/ │ └── database.go # Database configuration ├── server/ # Core server components │ ├── gateway.go # Main gateway server -│ ├── routes.go # Route registration +│ ├── routes.go # Route registration & middleware chain │ ├── api.go # API definitions │ ├── host.go # Host definitions │ └── target.go # Target (backend) definitions ├── middlewares/ # HTTP middleware +│ ├── auth.go # Authentication middleware │ ├── host.go # Host logging middleware │ └── logger.go # Structured logging middleware └── utils/ # Utility functions @@ -79,7 +80,7 @@ The Gateway is the main server component that: The Routes component handles: - Creating route handlers from configuration -- Applying strip prefix middleware +- Applying per-route middleware chain (Auth → StripPrefix) - Registering routes with chi router (method-agnostic) - Connecting routes to API backends @@ -101,9 +102,17 @@ Host definitions for: Target (backend) definitions that: - Store backend service URLs -- Create `httputil.ReverseProxy` instances +- Create `httputil.NewSingleHostReverseProxy` instances - Handle proxy configuration +#### Auth Middleware (`middlewares/auth.go`) + +Authentication middleware that: +- Validates presence of configured auth header (e.g., `X-Api-Key`, `Authorization`) +- Supports path-based filtering via include/exclude patterns +- Removes auth header before forwarding to backend +- Provides extensive DEBUG logging for troubleshooting + ## Middleware Chain ### Global Middleware @@ -119,19 +128,71 @@ Applied to all requests via chi middleware: ### Per-Route Middleware -Applied to each route handler: +Applied in order to each route handler: -1. **StripPrefix** - Removes specified prefix from request path before proxying +1. **Auth** (if enabled) - Validates authentication header with path filtering +2. **StripPrefix** (if enabled) - Removes specified prefix from request path before proxying ## Request Flow 1. **Client Request** → Gateway receives HTTP request 2. **Global Middleware** → Request ID, Real IP, Host logging, Logger applied 3. **Route Matching** → Chi matches route pattern (e.g., `/api/v1/*`) -4. **Per-Route Middleware** → StripPrefix (if enabled) +4. **Per-Route Middleware** → Auth → StripPrefix (if enabled) 5. **Reverse Proxy** → Request forwarded to backend API 6. **Response** → Backend response returned to client +## Authentication Flow + +The authentication middleware supports flexible path-based filtering: + +``` +┌─────────────────────────────────────┐ +│ Include and Exclude both empty? │ +└──────────────────┬──────────────────┘ + │ Yes + ┌─────────┴─────────┐ + │ Auth required │ + │ for ALL paths │ + └───────────────────┘ + + │ No + ┌─────────┴─────────┐ + ▼ │ +┌───────────────────┐ │ +│ Only Include set? │ │ +└─────────┬─────────┘ │ + │ Yes │ No │ + ▼ ▼ │ +┌────────┐ ┌────────────────┐│ +│ Auth │ │ Exclude set? ││ +│ ONLY │ └───────┬────────┘│ +│ for │ │ No │ +│ Include│ ┌────┴────┐ │ +│ paths │ │ Auth │ │ +└────────┘ │ for ALL │ │ + └────┬────┘ │ + │ Yes │ + ┌─────────┴─────────┐│ + │ Auth EXCEPT ││ + │ matching Exclude ││ + └───────────────────┘│ + │ + ▼ + Check Auth Header +``` + +**Path Filtering Logic:** +1. **Both include and exclude empty** → Auth required for ALL paths +2. **Only include set** → Auth required ONLY for paths matching include patterns +3. **Only exclude set** → Auth required for ALL paths EXCEPT those matching exclude patterns +4. **Both set** → Include takes precedence (same as #2) + +**Wildcard Pattern Matching:** +- `*` matches any path +- `/api/*` matches `/api/` and any subpath +- `/api/v1/public/test/*` matches the prefix and any subpath + ## Configuration Flow 1. Load `gateway.yaml` via `config.NewFile()` @@ -139,10 +200,10 @@ Applied to each route handler: 3. Create Hosts from configuration 4. Create Targets from configuration 5. Create APIs (linking Hosts to Targets) -6. Create Routes (linking Routes to APIs) +6. Create Routes (linking Routes to APIs with Auth config) 7. Initialize Gateway with all components 8. Configure proxy directors -9. Register routes with chi router +9. Register routes with chi router (including Auth middleware) 10. Start HTTP server ## Logging @@ -151,6 +212,7 @@ The gateway uses structured JSON logging via `masterlog`: - **HTTP Request Logging** - method, path, status, duration, host, IP - **Gateway Events** - startup, route registration, proxy configuration +- **Auth Debug Logs** - detailed auth decision logging when DEBUG level enabled - **Sensitive Field Pseudonymization** - user_id, email, ip fields are pseudonymized Example log output: @@ -166,3 +228,15 @@ Example log output: "ip": "127.0.0.1:52342" } ``` + +Auth debug logs (when DEBUG level enabled): +```json +{ + "level": "debug", + "msg": "AuthMiddleware: Checking if path requires auth", + "path": "/api/v1/users", + "requires_auth": true, + "include": [], + "exclude": ["/api/v1/public/*"] +} +``` diff --git a/.docs/configuration.md b/.docs/configuration.md index c3f3f48..23b8b70 100644 --- a/.docs/configuration.md +++ b/.docs/configuration.md @@ -36,6 +36,14 @@ routes: strip_prefix: enabled: true prefix: "/api/v1" + security: + auth: + enabled: true + type: "api_key" + header: "X-Api-Key" + path: + include: [] + exclude: [] ``` ## Sections @@ -93,7 +101,7 @@ Links hosts to backend targets. ### Routes -Route definitions with path patterns and prefix stripping. +Route definitions with path patterns, prefix stripping, and authentication. | Field | Type | Description | |-------|------|-------------| @@ -101,6 +109,7 @@ Route definitions with path patterns and prefix stripping. | `api` | string | API ID to use for this route | | `path` | string | Chi route pattern (e.g., `/api/v1/*`) | | `strip_prefix` | object | Prefix stripping configuration | +| `security` | object | Security policies (auth) | #### Strip Prefix @@ -109,60 +118,123 @@ Route definitions with path patterns and prefix stripping. | `enabled` | boolean | Enable prefix stripping | | `prefix` | string | Prefix to remove from path | +#### Security + +##### Authentication + +| Field | Type | Description | +|-------|------|-------------| +| `enabled` | boolean | Enable authentication | +| `type` | string | Auth type identifier (for documentation) | +| `header` | string | Header name to validate (e.g., `X-Api-Key`, `Authorization`) | +| `path` | object | Path-based filtering configuration | + +##### Auth Path Filtering + +| Field | Type | Description | +|-------|------|-------------| +| `include` | array | Paths that require auth (empty = all, if set only these) | +| `exclude` | array | Paths that skip auth (only used if include is empty) | + +**Path Filtering Logic:** + +| Include | Exclude | Behavior | +|---------|---------|----------| +| Empty | Empty | Auth required for ALL paths | +| Set | Empty | Auth required ONLY for paths matching include | +| Empty | Set | Auth required for ALL EXCEPT paths matching exclude | +| Set | Set | Include takes precedence (same as "Set, Empty") | + +**Wildcards in Path Patterns:** +- `*` matches any path +- `/api/*` matches `/api/` and any subpath +- `/api/v1/public/test/*` matches the prefix and any subpath + ## Example Configurations -### Simple Proxy (No Prefix Stripping) +### Public API (No Auth) ```yaml -gateway: - host: "0.0.0.0" - port: 8080 - features: - - logger - -hosts: - - id: "host-001" - name: "localhost" - domain: "localhost:8080" - -targets: - - id: "target-001" - name: "backend" - url: "https://api.example.com" - -apis: - - id: "api-001" - host: "host-001" - target: "target-001" - routes: - - id: "route-001" + - id: "public-api" api: "api-001" - path: "/api/*" + path: "/public/*" + strip_prefix: + enabled: true + prefix: "/public" + security: + auth: + enabled: false ``` -**Request flow:** -- Client requests: `/api/users/123` -- Backend receives: `/api/users/123` - -### Prefix Stripping +### Protected API (All Paths Require Auth) ```yaml routes: - - id: "route-001" + - id: "protected-api" api: "api-001" path: "/api/v1/*" strip_prefix: enabled: true prefix: "/api/v1" + security: + auth: + enabled: true + header: "X-Api-Key" + path: + include: [] + exclude: [] ``` -**Request flow:** -- Client requests: `/api/v1/users/123` -- Gateway strips: `/api/v1` -- Backend receives: `/users/123` +### Public with Protected Sub-paths (Include) -### Multiple Routes +```yaml +routes: + - id: "mixed-api" + api: "api-001" + path: "/api/*" + strip_prefix: + enabled: true + prefix: "/api" + security: + auth: + enabled: true + header: "X-Api-Key" + path: + include: ["/api/admin/*", "/api/users/*/profile"] + exclude: [] +``` + +In this example: +- `/api/admin/users` → Requires auth (matches include) +- `/api/users/123/profile` → Requires auth (matches include) +- `/api/public/data` → No auth (doesn't match include) +- `/api/health` → No auth (doesn't match include) + +### Protected with Public Sub-paths (Exclude) + +```yaml +routes: + - id: "protected-with-public" + api: "api-001" + path: "/api/*" + security: + auth: + enabled: true + header: "Authorization" + path: + include: [] + exclude: ["/api/health", "/api/public/*", "/api/v1/public/test/*"] +``` + +In this example: +- `/api/health` → No auth (matches exclude) +- `/api/public/data` → No auth (matches exclude) +- `/api/v1/public/test/123` → No auth (matches exclude) +- `/api/users` → Requires auth (doesn't match exclude) +- `/api/admin/settings` → Requires auth (doesn't match exclude) + +### Multiple Routes with Different Auth ```yaml routes: @@ -172,49 +244,37 @@ routes: strip_prefix: enabled: true prefix: "/public" + security: + auth: + enabled: false - - id: "api-route" + - id: "user-route" api: "api-001" - path: "/api/v1/*" + path: "/users/*" strip_prefix: enabled: true - prefix: "/api/v1" -``` + prefix: "/users" + security: + auth: + enabled: true + header: "X-Api-Key" + path: + include: [] + exclude: ["/users/login", "/users/register"] -### Multiple Backends - -```yaml -hosts: - - id: "host-001" - name: "api-host" - domain: "api.example.com" - - id: "host-002" - name: "admin-host" - domain: "admin.example.com" - -targets: - - id: "target-001" - name: "api-backend" - url: "https://api-backend.internal" - - id: "target-002" - name: "admin-backend" - url: "https://admin-backend.internal" - -apis: - - id: "api-001" - host: "host-001" - target: "target-001" - - id: "api-002" - host: "host-002" - target: "target-002" - -routes: - - id: "route-001" + - id: "admin-route" api: "api-001" - path: "/api/*" - - id: "route-002" - api: "api-002" path: "/admin/*" + strip_prefix: + enabled: true + prefix: "/admin" + security: + auth: + enabled: true + header: "Authorization" + path: + include: [] + exclude: [] ``` ## Configuration Loading @@ -244,3 +304,23 @@ The gateway uses chi/v5 routing patterns. Common patterns: | `/files/*` | `/files/` and any subpath | `/files/doc.pdf` | Note: `/*` matches zero or more path segments. + +## Debug Logging + +To see detailed authentication decisions, enable DEBUG logging in `main.go`: + +```go +masterlog.SetLevel(masterlog.LevelDebug) +``` + +This will output logs like: +```json +{ + "level": "debug", + "msg": "AuthMiddleware: Checking if path requires auth", + "path": "/api/v1/users", + "requires_auth": true, + "include": [], + "exclude": ["/api/v1/public/*"] +} +``` diff --git a/.docs/usage.md b/.docs/usage.md index b999881..24068c8 100644 --- a/.docs/usage.md +++ b/.docs/usage.md @@ -56,6 +56,109 @@ curl -X PUT http://localhost:8080/api/v1/data/123 curl -X DELETE http://localhost:8080/api/v1/data/123 ``` +## Authentication + +### Using API Key Authentication + +**Configuration:** +```yaml +security: + auth: + enabled: true + header: "X-Api-Key" +``` + +**Valid request:** +```bash +curl -H "X-Api-Key: your-secret-key" http://localhost:8080/api/v1/data +``` + +**Invalid request (missing header):** +```bash +curl http://localhost:8080/api/v1/data +# Returns: 401 Unauthorized +``` + +### Using Bearer Token Authentication + +**Configuration:** +```yaml +security: + auth: + enabled: true + header: "Authorization" +``` + +**Valid request:** +```bash +curl -H "Authorization: Bearer your-token-here" http://localhost:8080/api/v1/data +``` + +### Path-Based Authentication Examples + +#### All Paths Require Auth + +**Configuration:** +```yaml +security: + auth: + enabled: true + header: "X-Api-Key" + path: + include: [] + exclude: [] +``` + +All requests to this route require authentication. + +#### Only Specific Paths Require Auth (Include) + +**Configuration:** +```yaml +security: + auth: + enabled: true + header: "X-Api-Key" + path: + include: ["/api/admin/*", "/api/users/*/profile"] + exclude: [] +``` + +**Request examples:** +```bash +# Requires auth (matches include) +curl -H "X-Api-Key: key" http://localhost:8080/api/admin/users +curl -H "X-Api-Key: key" http://localhost:8080/api/users/123/profile + +# No auth required (doesn't match include) +curl http://localhost:8080/api/public/data +curl http://localhost:8080/api/health +``` + +#### All Paths Except Specific Ones Require Auth (Exclude) + +**Configuration:** +```yaml +security: + auth: + enabled: true + header: "X-Api-Key" + path: + include: [] + exclude: ["/api/health", "/api/public/*"] +``` + +**Request examples:** +```bash +# No auth required (matches exclude) +curl http://localhost:8080/api/health +curl http://localhost:8080/api/public/data + +# Requires auth (doesn't match exclude) +curl -H "X-Api-Key: key" http://localhost:8080/api/users +curl -H "X-Api-Key: key" http://localhost:8080/api/admin/settings +``` + ## Route Examples ### Strip Prefix Example @@ -87,6 +190,9 @@ routes: strip_prefix: enabled: true prefix: "/public" + security: + auth: + enabled: false - id: "api-route" api: "api-001" @@ -94,15 +200,19 @@ routes: strip_prefix: enabled: true prefix: "/api/v1" + security: + auth: + enabled: true + header: "X-Api-Key" ``` **Requests:** ```bash -# Public route - backend receives /status +# Public route - no auth, backend receives /status curl http://localhost:8080/public/status -# API route - backend receives /users -curl http://localhost:8080/api/v1/users +# API route - requires auth, backend receives /users +curl -H "X-Api-Key: key" http://localhost:8080/api/v1/users ``` ## Logging @@ -130,6 +240,27 @@ The gateway uses structured JSON logging via `masterlog`. All HTTP requests are } ``` +### Debug Logging + +To enable detailed authentication logging, set DEBUG level in `main.go`: + +```go +masterlog.SetLevel(masterlog.LevelDebug) +``` + +This will output detailed auth decision logs: + +```json +{ + "level": "debug", + "msg": "AuthMiddleware: Checking if path requires auth", + "path": "/api/v1/users", + "requires_auth": true, + "include": [], + "exclude": ["/api/v1/public/*"] +} +``` + ## Troubleshooting ### Gateway fails to start @@ -146,6 +277,13 @@ cat ../gateway.yaml 3. Check logs for detailed error messages +### 401 Unauthorized + +- Verify auth header is included +- Check header name matches configuration (case-sensitive) +- Verify path is not excluded from auth (check exclude patterns) +- Enable DEBUG logging to see auth decision process + ### 404 Not Found - Verify the route path matches your request @@ -165,6 +303,22 @@ cat ../gateway.yaml - Remember that `/*` matches zero or more path segments - Check that multiple routes don't have conflicting patterns +### Auth not working as expected + +1. Enable DEBUG logging to see auth decisions: +```go +masterlog.SetLevel(masterlog.LevelDebug) +``` + +2. Check the logs for: + - Whether the path requires auth + - Whether the auth header is present + - Which patterns are being matched + +3. Verify your include/exclude patterns: + - Wildcards end with `*` + - Patterns are prefix-based (e.g., `/api/v1/public/test/*`) + ## Performance Considerations - The gateway uses Go's `httputil.ReverseProxy` for efficient proxying @@ -175,23 +329,57 @@ cat ../gateway.yaml ## Common Patterns -### API Versioning +### API Versioning with Auth ```yaml routes: - - id: "v1-api" + - id: "v1-public-api" + api: "api-001" + path: "/api/v1/public/*" + strip_prefix: + enabled: true + prefix: "/api/v1/public" + security: + auth: + enabled: false + + - id: "v1-protected-api" api: "api-001" path: "/api/v1/*" strip_prefix: enabled: true prefix: "/api/v1" + security: + auth: + enabled: true + header: "X-Api-Key" +``` - - id: "v2-api" +### Mixed Authentication + +```yaml +routes: + - id: "user-api" api: "api-001" - path: "/api/v2/*" - strip_prefix: - enabled: true - prefix: "/api/v2" + path: "/users/*" + security: + auth: + enabled: true + header: "X-Api-Key" + path: + include: [] + exclude: ["/users/login", "/users/register"] + + - id: "admin-api" + api: "api-001" + path: "/admin/*" + security: + auth: + enabled: true + header: "Authorization" + path: + include: [] + exclude: [] ``` ### Microservices Routing @@ -220,6 +408,10 @@ routes: strip_prefix: enabled: true prefix: "/users" + security: + auth: + enabled: true + header: "X-Api-Key" - id: "orders-route" api: "order-api" @@ -227,28 +419,8 @@ routes: strip_prefix: enabled: true prefix: "/orders" + security: + auth: + enabled: true + header: "Authorization" ``` - -### External API Proxy - -```yaml -targets: - - id: "external-api" - name: "External API" - url: "https://api.external.com" - -apis: - - id: "external" - host: "host-001" - target: "external-api" - -routes: - - id: "external-proxy" - api: "external" - path: "/proxy/external/*" - strip_prefix: - enabled: true - prefix: "/proxy/external" -``` - -Request to `/proxy/external/users` becomes `/users` on the external API. diff --git a/app/middlewares/auth.go b/app/middlewares/auth.go new file mode 100644 index 0000000..d76bfef --- /dev/null +++ b/app/middlewares/auth.go @@ -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 +} diff --git a/app/server/routes.go b/app/server/routes.go index d402ee6..773675b 100644 --- a/app/server/routes.go +++ b/app/server/routes.go @@ -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) }