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
19 changes: 19 additions & 0 deletions docs/gateway/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,25 @@ any-llm-gateway offers two authentication methods, each designed for different u
| **Master Key** | Internal services, admin operations, trusted environments | Single key with full access | Requires manual user specification |
| **Virtual API Keys** | External apps, per-user access, customer-facing services | Multiple scoped keys | Automatic per-key tracking |

### Supported Headers

The gateway accepts authentication via two headers:

- **`X-AnyLLM-Key`** (preferred): The gateway's native authentication header
- **`Authorization`**: Standard HTTP authorization header for OpenAI client compatibility

Both headers use the `Bearer <token>` format. When both headers are present, `X-AnyLLM-Key` takes precedence.

Using the `Authorization` header allows you to use the gateway with OpenAI-compatible clients without modification:

```python
from openai import OpenAI

client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="your-master-key-or-virtual-key", # Sent as Authorization: Bearer ...
)
```

## Master Key
The master key is the root credential for your gateway installation. It has unrestricted access to all gateway operations and should be treated with the same security as your production database credentials.
Expand Down
18 changes: 11 additions & 7 deletions src/any_llm/gateway/auth/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,26 @@ def get_config() -> GatewayConfig:


def _extract_bearer_token(request: Request, config: GatewayConfig) -> str:
"""Extract and validate Bearer token from request header."""
x_anyllm_key = request.headers.get(API_KEY_HEADER)
"""Extract and validate Bearer token from request header.

if not x_anyllm_key:
Checks X-AnyLLM-Key first, then falls back to standard Authorization header
for OpenAI client compatibility.
"""
auth_header = request.headers.get(API_KEY_HEADER) or request.headers.get("Authorization")

if not auth_header:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Missing {API_KEY_HEADER} header",
detail=f"Missing {API_KEY_HEADER} or Authorization header",
)

if not x_anyllm_key.startswith("Bearer "):
if not auth_header.startswith("Bearer "):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid {API_KEY_HEADER} header format. Expected 'Bearer <token>'",
detail="Invalid header format. Expected 'Bearer <token>'",
)

return x_anyllm_key[7:]
return auth_header[7:]


def _verify_and_update_api_key(db: Session, token: str) -> APIKey:
Expand Down
16 changes: 16 additions & 0 deletions tests/gateway/test_key_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,19 @@ def test_inactive_api_key_rejected(
headers={API_KEY_HEADER: f"Bearer {api_key['key']}"},
)
assert response.status_code == 401


def test_authorization_header_accepted(client: TestClient, test_config: GatewayConfig) -> None:
"""Test that Authorization header works as fallback for OpenAI client compatibility."""
# Use Authorization header instead of X-AnyLLM-Key
auth_header = {"Authorization": f"Bearer {test_config.master_key}"}

response = client.post(
"/v1/keys",
json={"key_name": "auth-header-test"},
headers=auth_header,
)

assert response.status_code == 200
data = response.json()
assert data["key_name"] == "auth-header-test"
Loading