diff --git a/.env.example b/.env.example index 95e795e..0d4d1e3 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,12 @@ MOCK_RAG=true PORT=8000 APP_NAME=Chat UI 13 +# Authentication configuration +# Header name to extract authenticated username from reverse proxy +# Different reverse proxy setups use different header names (e.g., X-User-Email, X-Authenticated-User, X-Remote-User) +# Default: X-User-Email +# AUTH_USER_HEADER=X-User-Email + # Agent mode configuration AGENT_MAX_STEPS=10 AGENT_DEFAULT_ENABLED=true diff --git a/backend/core/auth.py b/backend/core/auth.py index 40e1abb..37b1b61 100644 --- a/backend/core/auth.py +++ b/backend/core/auth.py @@ -61,7 +61,7 @@ async def is_user_in_group(user_id: str, group_id: str) -> bool: def get_user_from_header(x_email_header: Optional[str]) -> Optional[str]: - """Extract user email from X-User-Email header.""" + """Extract user email from authentication header value.""" if not x_email_header: return None return x_email_header.strip() \ No newline at end of file diff --git a/backend/core/middleware.py b/backend/core/middleware.py index 48759d8..9aec23d 100644 --- a/backend/core/middleware.py +++ b/backend/core/middleware.py @@ -17,9 +17,10 @@ class AuthMiddleware(BaseHTTPMiddleware): """Middleware to handle authentication and logging.""" - def __init__(self, app, debug_mode: bool = False): + def __init__(self, app, debug_mode: bool = False, auth_header_name: str = "X-User-Email"): super().__init__(app) self.debug_mode = debug_mode + self.auth_header_name = auth_header_name async def dispatch(self, request: Request, call_next) -> Response: # Log request @@ -50,11 +51,11 @@ async def dispatch(self, request: Request, call_next) -> Response: else: logger.warning("Invalid capability token provided") - # Check authentication via X-User-Email header + # Check authentication via configured header (default: X-User-Email) user_email = None if self.debug_mode: - # In debug mode, honor X-User-Email header if provided, otherwise use config test user - x_email_header = request.headers.get('X-User-Email') + # In debug mode, honor auth header if provided, otherwise use config test user + x_email_header = request.headers.get(self.auth_header_name) if x_email_header: user_email = get_user_from_header(x_email_header) else: @@ -63,7 +64,7 @@ async def dispatch(self, request: Request, call_next) -> Response: user_email = config_manager.app_settings.test_user # logger.info(f"Debug mode: using user {user_email}") else: - x_email_header = request.headers.get('X-User-Email') + x_email_header = request.headers.get(self.auth_header_name) user_email = get_user_from_header(x_email_header) if not user_email: @@ -72,7 +73,7 @@ async def dispatch(self, request: Request, call_next) -> Response: logger.warning(f"Missing authentication for API endpoint: {request.url.path}") raise HTTPException(status_code=401, detail="Unauthorized") else: - logger.warning("Missing X-User-Email, redirecting to auth") + logger.warning(f"Missing {self.auth_header_name}, redirecting to auth") return RedirectResponse(url="/auth", status_code=302) # Add user to request state diff --git a/backend/main.py b/backend/main.py index f7c02cc..ed0c81e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -123,7 +123,11 @@ async def lifespan(app: FastAPI): """ app.add_middleware(SecurityHeadersMiddleware) app.add_middleware(RateLimitMiddleware) -app.add_middleware(AuthMiddleware, debug_mode=config.app_settings.debug_mode) +app.add_middleware( + AuthMiddleware, + debug_mode=config.app_settings.debug_mode, + auth_header_name=config.app_settings.auth_user_header +) # Include essential routes (add files API) app.include_router(config_router) @@ -186,9 +190,12 @@ async def websocket_endpoint(websocket: WebSocket): 2. Reverse proxy intercepts WebSocket handshake (HTTP Upgrade request) 3. Reverse proxy delegates to authentication service 4. Auth service validates JWT/session from cookies or headers - 5. If valid: Auth service returns X-Authenticated-User header - 6. Reverse proxy forwards connection to this app with X-Authenticated-User header + 5. If valid: Auth service returns authenticated user header + 6. Reverse proxy forwards connection to this app with authenticated user header 7. This app trusts the header (already validated by auth service) + + The header name is configurable via AUTH_USER_HEADER environment variable + (default: X-User-Email). This allows flexibility for different reverse proxy setups. SECURITY REQUIREMENTS: - This app MUST ONLY be accessible via reverse proxy @@ -197,7 +204,7 @@ async def websocket_endpoint(websocket: WebSocket): - The /login endpoint lives in the separate auth service DEVELOPMENT vs PRODUCTION: - - Production: Extracts user from X-Authenticated-User header (set by reverse proxy) + - Production: Extracts user from configured auth header (set by reverse proxy) - Development: Falls back to 'user' query parameter (INSECURE, local only) See docs/security_architecture.md for complete architecture details. diff --git a/backend/modules/config/config_manager.py b/backend/modules/config/config_manager.py index 971cb84..7f811e6 100644 --- a/backend/modules/config/config_manager.py +++ b/backend/modules/config/config_manager.py @@ -198,6 +198,13 @@ def agent_mode_available(self) -> bool: auth_group_check_url: Optional[str] = Field(default=None, validation_alias="AUTH_GROUP_CHECK_URL") auth_group_check_api_key: Optional[str] = Field(default=None, validation_alias="AUTH_GROUP_CHECK_API_KEY") + # Authentication header configuration + auth_user_header: str = Field( + default="X-User-Email", + description="HTTP header name to extract authenticated username from reverse proxy", + validation_alias="AUTH_USER_HEADER" + ) + # S3/MinIO storage settings use_mock_s3: bool = False # Use in-process S3 mock (no Docker required) s3_endpoint: str = "http://localhost:9000" diff --git a/backend/tests/test_middleware_auth.py b/backend/tests/test_middleware_auth.py index 74172f6..b78bd46 100644 --- a/backend/tests/test_middleware_auth.py +++ b/backend/tests/test_middleware_auth.py @@ -33,3 +33,57 @@ def auth(): assert resp.url.path == "/auth" else: assert resp.status_code == expected_status + + +def test_auth_middleware_custom_header(): + """Test that custom auth header name can be configured.""" + from fastapi import Request + + app = FastAPI() + + @app.get("/ping") + def ping(request: Request): + # Return the authenticated user email + return {"user": request.state.user_email} + + # Add an /auth route to receive redirects + @app.get("/auth") + def auth(): + return {"login": True} + + # Use a custom header name + app.add_middleware(AuthMiddleware, debug_mode=False, auth_header_name="X-Authenticated-User") + client = TestClient(app) + + # Test with the custom header + headers = {"X-Authenticated-User": "custom@example.com"} + resp = client.get("/ping", headers=headers) + assert resp.status_code == 200 + assert resp.json()["user"] == "custom@example.com" + + # Test that the old header doesn't work + headers = {"X-User-Email": "old@example.com"} + resp = client.get("/ping", headers=headers) + # Should redirect because the configured header is missing + assert resp.url.path == "/auth" + + +def test_auth_middleware_custom_header_debug_mode(): + """Test that custom auth header works in debug mode.""" + from fastapi import Request + + app = FastAPI() + + @app.get("/ping") + def ping(request: Request): + return {"user": request.state.user_email} + + app.add_middleware(AuthMiddleware, debug_mode=True, auth_header_name="X-Remote-User") + client = TestClient(app) + + # Test with the custom header + headers = {"X-Remote-User": "debug@example.com"} + resp = client.get("/ping", headers=headers) + assert resp.status_code == 200 + assert resp.json()["user"] == "debug@example.com" + diff --git a/docs/02_admin_guide.md b/docs/02_admin_guide.md index c45ab30..ca1c202 100644 --- a/docs/02_admin_guide.md +++ b/docs/02_admin_guide.md @@ -62,6 +62,7 @@ cp .env.example .env Key settings in the `.env` file include: * **API Keys**: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc. +* **Authentication Header**: `AUTH_USER_HEADER` configures the HTTP header name used to extract the authenticated username from your reverse proxy (default: `X-User-Email`). * **Feature Flags**: Enable or disable major features like `FEATURE_AGENT_MODE_AVAILABLE`. * **S3 Connection**: Configure the connection to your S3-compatible storage. For local testing, you can set `USE_MOCK_S3=true` to use an in-memory mock instead of a real S3 bucket. **This mock must never be used in production.** * **Log Directory**: The `APP_LOG_DIR` variable points to the folder where the application log file (`app.jsonl`) will be stored. This path must be updated to a valid directory in your deployment environment. @@ -234,13 +235,36 @@ The intended flow for user authentication in a production environment is as foll 1. The user makes a request to the application's public URL, which is handled by the **Reverse Proxy**. 2. The Reverse Proxy communicates with an **Authentication Service** (e.g., an SSO provider, an OAuth server) to validate the user's credentials (like cookies or tokens). -3. Once the user is authenticated, the Reverse Proxy **injects the user's identity** (e.g., their email address) into the `x-email-header` and forwards the request to the **Atlas UI Backend**. +3. Once the user is authenticated, the Reverse Proxy **injects the user's identity** (e.g., their email address) into an HTTP header and forwards the request to the **Atlas UI Backend**. -The backend application then reads this header to identify the user. This model is secure only if the backend is not directly exposed to the internet, ensuring that all requests are processed by the proxy first. +The backend application reads this header to identify the user. The header name is configurable via the `AUTH_USER_HEADER` environment variable (default: `X-User-Email`). This allows flexibility for different reverse proxy setups that may use different header names (e.g., `X-Authenticated-User`, `X-Remote-User`). This model is secure only if the backend is not directly exposed to the internet, ensuring that all requests are processed by the proxy first. ### Development Behavior -In a local development environment (when `DEBUG_MODE=true` in the `.env` file), the system falls back to using a default `test@test.com` user if the `x-email-header` is not present. +In a local development environment (when `DEBUG_MODE=true` in the `.env` file), the system falls back to using a default `test@test.com` user if the configured authentication header is not present. + +### Configuring the Authentication Header + +Different reverse proxy setups use different header names to pass authenticated user information. The application supports configuring the header name via the `AUTH_USER_HEADER` environment variable. + +**Default Configuration:** +``` +AUTH_USER_HEADER=X-User-Email +``` + +**Common Alternative Headers:** +``` +# For Apache mod_auth setups +AUTH_USER_HEADER=X-Remote-User + +# For some SSO providers +AUTH_USER_HEADER=X-Authenticated-User + +# For custom reverse proxy configurations +AUTH_USER_HEADER=X-Custom-Auth-Header +``` + +This setting allows the application to work with various authentication infrastructures without code changes. ### Customizing Authorization