diff --git a/agentlightning/server/app.py b/agentlightning/server/app.py index 9f94a59e8..1b90eae51 100644 --- a/agentlightning/server/app.py +++ b/agentlightning/server/app.py @@ -39,7 +39,9 @@ async def verify_key(request: Request) -> None: return auth_header = request.headers.get("authorization", "") - if auth_header.startswith("Bearer ") and auth_header[7:] == key: + # RFC 7235 ยง2.1: auth-scheme is case-insensitive; allow one or more spaces. + parts = auth_header.split(None, 1) + if len(parts) == 2 and parts[0].casefold() == "bearer" and parts[1] == key: return if request.headers.get("x-api-key", "") == key: diff --git a/tests/server/test_endpoints.py b/tests/server/test_endpoints.py index e23fadfae..559947f4f 100644 --- a/tests/server/test_endpoints.py +++ b/tests/server/test_endpoints.py @@ -8,7 +8,7 @@ import pytest from fastapi.testclient import TestClient -from tests.server.conftest import MODEL_NAME +from tests.server.conftest import AGL_KEY, MODEL_NAME def _rollout(client: TestClient, headers: dict[str, str]) -> dict: @@ -40,6 +40,62 @@ def test_auth_required(client: TestClient): assert client.post("/api/rollouts", json=[]).status_code == 401 +@pytest.mark.parametrize( + "authorization", + [ + f"Bearer {AGL_KEY}", + f"bearer {AGL_KEY}", + f"BEARER {AGL_KEY}", + f"BeArEr {AGL_KEY}", + f"Bearer {AGL_KEY}", + ], +) +def test_bearer_auth_accepts_case_insensitive_scheme(client: TestClient, authorization: str): + # Hit the real auth dependency through a store route (issue #671). + response = client.get("/api/rollouts", params={"state_in": "queuing"}, headers={"Authorization": authorization}) + assert response.status_code == 200 + assert response.json() == [] + + +def test_x_api_key_auth_still_accepted(client: TestClient): + response = client.get( + "/api/rollouts", + params={"state_in": "queuing"}, + headers={"x-api-key": AGL_KEY}, + ) + assert response.status_code == 200 + assert response.json() == [] + + +@pytest.mark.parametrize( + "headers", + [ + {}, + {"Authorization": f"Bearer wrong-{AGL_KEY}"}, + {"Authorization": f"bearer wrong-{AGL_KEY}"}, + {"Authorization": f"Basic {AGL_KEY}"}, + {"Authorization": f"Token {AGL_KEY}"}, + {"Authorization": "Bearer"}, + {"Authorization": AGL_KEY}, + {"x-api-key": f"wrong-{AGL_KEY}"}, + ], + ids=[ + "missing", + "wrong-key-canonical", + "wrong-key-lower", + "basic-scheme", + "token-scheme", + "scheme-only", + "bare-key", + "wrong-x-api-key", + ], +) +def test_auth_rejects_invalid_credentials(client: TestClient, headers: dict[str, str]): + response = client.get("/api/rollouts", params={"state_in": "queuing"}, headers=headers) + assert response.status_code == 401 + assert response.json() == {"detail": "Invalid or missing API key"} + + def test_rollout_endpoints(client: TestClient, auth_headers: dict[str, str]): rollout = _rollout(client, auth_headers) assert rollout["input"] == {"prompt": "hi"}