Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion backend/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
13 changes: 7 additions & 6 deletions backend/core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand Down
15 changes: 11 additions & 4 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions backend/modules/config/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
54 changes: 54 additions & 0 deletions backend/tests/test_middleware_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

30 changes: 27 additions & 3 deletions docs/02_admin_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
Loading