From 3698a54d485151cd33e44677f7b635930b5ee3e9 Mon Sep 17 00:00:00 2001 From: 13ernkastel Date: Thu, 26 Mar 2026 15:33:00 +0800 Subject: [PATCH 1/2] Enforce auth on bot proxy chat endpoints --- openviking/server/routers/bot.py | 20 +++++++++---- tests/server/test_bot_proxy_auth.py | 46 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 tests/server/test_bot_proxy_auth.py diff --git a/openviking/server/routers/bot.py b/openviking/server/routers/bot.py index d11cbf528..c2090eeff 100644 --- a/openviking/server/routers/bot.py +++ b/openviking/server/routers/bot.py @@ -52,6 +52,16 @@ async def verify_auth(request: Request) -> Optional[str]: return None +def require_auth_token(auth_token: Optional[str]) -> str: + """Enforce auth token presence for bot proxy endpoints.""" + if not auth_token: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Missing authentication token", + ) + return auth_token + + @router.get("/health") async def health_check(request: Request): """Health check endpoint for Bot API. @@ -92,7 +102,7 @@ async def chat(request: Request): Proxies the request to Vikingbot OpenAPIChannel. """ bot_url = get_bot_url() - auth_token = await verify_auth(request) + auth_token = require_auth_token(await verify_auth(request)) # Read request body try: @@ -107,8 +117,7 @@ async def chat(request: Request): async with httpx.AsyncClient() as client: # Build headers headers = {"Content-Type": "application/json"} - if auth_token: - headers["X-API-Key"] = auth_token + headers["X-API-Key"] = auth_token # Forward to Vikingbot OpenAPIChannel chat endpoint response = await client.post( @@ -146,7 +155,7 @@ async def chat_stream(request: Request): Proxies the request to Vikingbot OpenAPIChannel with SSE streaming. """ bot_url = get_bot_url() - auth_token = await verify_auth(request) + auth_token = require_auth_token(await verify_auth(request)) # Read request body try: @@ -163,8 +172,7 @@ async def event_stream() -> AsyncGenerator[str, None]: async with httpx.AsyncClient() as client: # Build headers headers = {"Content-Type": "application/json"} - if auth_token: - headers["X-API-Key"] = auth_token + headers["X-API-Key"] = auth_token # Forward to Vikingbot OpenAPIChannel stream endpoint async with client.stream( diff --git a/tests/server/test_bot_proxy_auth.py b/tests/server/test_bot_proxy_auth.py new file mode 100644 index 000000000..1defc845f --- /dev/null +++ b/tests/server/test_bot_proxy_auth.py @@ -0,0 +1,46 @@ +# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. +# SPDX-License-Identifier: Apache-2.0 + +"""Regression tests for bot proxy endpoint auth enforcement.""" + +import httpx +import pytest +import pytest_asyncio +from fastapi import FastAPI + +import openviking.server.routers.bot as bot_router_module + + +@pytest_asyncio.fixture +async def bot_auth_client() -> httpx.AsyncClient: + """Client mounted with bot router and bot backend configured.""" + app = FastAPI() + bot_router_module.set_bot_api_url("http://bot-backend.local") + app.include_router(bot_router_module.router, prefix="/bot/v1") + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: + yield client + + +@pytest.mark.asyncio +async def test_chat_requires_auth_token(bot_auth_client: httpx.AsyncClient): + """POST /bot/v1/chat should reject missing auth with 401.""" + response = await bot_auth_client.post( + "/bot/v1/chat", + json={"message": "hello"}, + ) + + assert response.status_code == 401 + assert response.json()["detail"] == "Missing authentication token" + + +@pytest.mark.asyncio +async def test_chat_stream_requires_auth_token(bot_auth_client: httpx.AsyncClient): + """POST /bot/v1/chat/stream should reject missing auth with 401.""" + response = await bot_auth_client.post( + "/bot/v1/chat/stream", + json={"message": "hello"}, + ) + + assert response.status_code == 401 + assert response.json()["detail"] == "Missing authentication token" From 8f989cebd94e0ce19ed8ac1a81fd9c3401688dcf Mon Sep 17 00:00:00 2001 From: 13ernkastel Date: Fri, 27 Mar 2026 14:21:58 +0800 Subject: [PATCH 2/2] Refine bot auth helper naming and test coverage --- openviking/server/routers/bot.py | 17 ++++---- tests/server/test_bot_proxy_auth.py | 60 +++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/openviking/server/routers/bot.py b/openviking/server/routers/bot.py index c2090eeff..538146a52 100644 --- a/openviking/server/routers/bot.py +++ b/openviking/server/routers/bot.py @@ -37,7 +37,7 @@ def get_bot_url() -> str: return BOT_API_URL -async def verify_auth(request: Request) -> Optional[str]: +def extract_auth_token(request: Request) -> Optional[str]: """Extract and return authorization token from request.""" # Try X-API-Key header first api_key = request.headers.get("X-API-Key") @@ -52,8 +52,9 @@ async def verify_auth(request: Request) -> Optional[str]: return None -def require_auth_token(auth_token: Optional[str]) -> str: - """Enforce auth token presence for bot proxy endpoints.""" +def require_auth_token(request: Request) -> str: + """Return an auth token or raise 401 for bot proxy endpoints.""" + auth_token = extract_auth_token(request) if not auth_token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -102,7 +103,7 @@ async def chat(request: Request): Proxies the request to Vikingbot OpenAPIChannel. """ bot_url = get_bot_url() - auth_token = require_auth_token(await verify_auth(request)) + auth_token = require_auth_token(request) # Read request body try: @@ -116,8 +117,7 @@ async def chat(request: Request): try: async with httpx.AsyncClient() as client: # Build headers - headers = {"Content-Type": "application/json"} - headers["X-API-Key"] = auth_token + headers = {"Content-Type": "application/json", "X-API-Key": auth_token} # Forward to Vikingbot OpenAPIChannel chat endpoint response = await client.post( @@ -155,7 +155,7 @@ async def chat_stream(request: Request): Proxies the request to Vikingbot OpenAPIChannel with SSE streaming. """ bot_url = get_bot_url() - auth_token = require_auth_token(await verify_auth(request)) + auth_token = require_auth_token(request) # Read request body try: @@ -171,8 +171,7 @@ async def event_stream() -> AsyncGenerator[str, None]: try: async with httpx.AsyncClient() as client: # Build headers - headers = {"Content-Type": "application/json"} - headers["X-API-Key"] = auth_token + headers = {"Content-Type": "application/json", "X-API-Key": auth_token} # Forward to Vikingbot OpenAPIChannel stream endpoint async with client.stream( diff --git a/tests/server/test_bot_proxy_auth.py b/tests/server/test_bot_proxy_auth.py index 1defc845f..2f7f86442 100644 --- a/tests/server/test_bot_proxy_auth.py +++ b/tests/server/test_bot_proxy_auth.py @@ -6,39 +6,69 @@ import httpx import pytest import pytest_asyncio -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException, Request import openviking.server.routers.bot as bot_router_module +def make_request(headers: dict[str, str]) -> Request: + """Create a minimal request object with the provided headers.""" + return Request( + { + "type": "http", + "method": "POST", + "path": "/", + "headers": [ + (key.lower().encode("latin-1"), value.encode("latin-1")) + for key, value in headers.items() + ], + "query_string": b"", + } + ) + + @pytest_asyncio.fixture async def bot_auth_client() -> httpx.AsyncClient: """Client mounted with bot router and bot backend configured.""" app = FastAPI() + old_bot_api_url = bot_router_module.BOT_API_URL bot_router_module.set_bot_api_url("http://bot-backend.local") app.include_router(bot_router_module.router, prefix="/bot/v1") transport = httpx.ASGITransport(app=app) - async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: - yield client + try: + async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: + yield client + finally: + bot_router_module.BOT_API_URL = old_bot_api_url -@pytest.mark.asyncio -async def test_chat_requires_auth_token(bot_auth_client: httpx.AsyncClient): - """POST /bot/v1/chat should reject missing auth with 401.""" - response = await bot_auth_client.post( - "/bot/v1/chat", - json={"message": "hello"}, - ) +@pytest.mark.parametrize( + ("headers", "expected"), + [ + ({"X-API-Key": "test-key"}, "test-key"), + ({"Authorization": "Bearer test-token"}, "test-token"), + ], +) +def test_extract_auth_token(headers: dict[str, str], expected: str): + """Accepted auth header formats should both produce a token.""" + assert bot_router_module.extract_auth_token(make_request(headers)) == expected - assert response.status_code == 401 - assert response.json()["detail"] == "Missing authentication token" + +def test_require_auth_token_rejects_missing_token(): + """Missing credentials should raise a 401 before proxying.""" + with pytest.raises(HTTPException) as exc_info: + bot_router_module.require_auth_token(make_request({})) + + assert exc_info.value.status_code == 401 + assert exc_info.value.detail == "Missing authentication token" @pytest.mark.asyncio -async def test_chat_stream_requires_auth_token(bot_auth_client: httpx.AsyncClient): - """POST /bot/v1/chat/stream should reject missing auth with 401.""" +@pytest.mark.parametrize("path", ["/bot/v1/chat", "/bot/v1/chat/stream"]) +async def test_bot_proxy_requires_auth_token(bot_auth_client: httpx.AsyncClient, path: str): + """Bot proxy endpoints should reject missing auth with 401.""" response = await bot_auth_client.post( - "/bot/v1/chat/stream", + path, json={"message": "hello"}, )