From 2055272ad4c89fa71e18662543a9b45b26ef2f47 Mon Sep 17 00:00:00 2001 From: KVR Vishnu <46535652+VISHNU0906@users.noreply.github.com> Date: Fri, 6 Nov 2026 13:23:40 +0530 Subject: [PATCH] fix(server): accept case-insensitive Bearer auth scheme RFC 7235 makes the Authorization auth-scheme case-insensitive. Compare the scheme with casefold() and allow one or more spaces before the key so lowercase/mixed-case clients authenticate. Fixes #671 Signed-off-by: KVR Vishnu <46535652+VISHNU0906@users.noreply.github.com> --- agentlightning/server/app.py | 4 ++- tests/server/test_endpoints.py | 58 +++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) 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"}