fix(vertex): read the caller's key only from x-litellm-api-key or Authorization on Vertex routes

The single auth check on /vertex_ai and /vertex-ai now reads the same two headers main's effective check did, so a key sent only in another key header gets 401 again instead of authenticating.
This commit is contained in:
mateo-berri 2026-10-20 21:32:47 -07:00
parent 60e16040fa
commit f0f046db12
2 changed files with 35 additions and 3 deletions

View file

@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Annotated, Final, Literal, Protocol, cast
import httpx
import openai
from fastapi import APIRouter, Depends, HTTPException, Request, Response, WebSocket
from fastapi import APIRouter, Depends, HTTPException, Request, Response, Security, WebSocket
from fastapi.responses import StreamingResponse
from pydantic import ConfigDict, TypeAdapter, ValidationError
from starlette.websockets import WebSocketState
@ -77,6 +77,8 @@ from litellm.proxy.auth.handle_jwt import JWTHandler
from litellm.proxy.auth.route_checks import RouteChecks
from litellm.proxy.auth.user_api_key_auth import ( # noqa: F401 # legacy module exports
_get_bearer_token, # pyright: ignore[reportPrivateUsage,reportUnusedImport] # backwards-compatible package export
api_key_header,
custom_litellm_key_header,
get_bearer_token,
is_no_auth_dev_mode,
user_api_key_auth,
@ -3137,6 +3139,16 @@ async def vertex_discovery_proxy_route(
)
async def _authenticate_vertex_caller(
request: Request,
authorization: Annotated[str | None, Security(api_key_header)],
litellm_api_key: Annotated[str | None, Security(custom_litellm_key_header)],
) -> UserAPIKeyAuth:
return await user_api_key_auth(
request=request, api_key=authorization or "", custom_litellm_key_header=litellm_api_key
)
@router.api_route(
"/vertex-ai/{endpoint:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
@ -3152,7 +3164,7 @@ async def vertex_proxy_route(
endpoint: str,
request: Request,
fastapi_response: Response,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(_authenticate_vertex_caller)],
):
"""
Call LiteLLM proxy via Vertex AI SDK.

View file

@ -8128,11 +8128,14 @@ class TestAzureSpeechRawBodyThroughRealAuth:
class _CountingCustomAuth:
def __init__(self) -> None:
def __init__(self, only_accepts: str | None = None) -> None:
self.only_accepts: Final = only_accepts
self.api_keys_seen: list[str] = []
async def __call__(self, request: Request, api_key: str) -> UserAPIKeyAuth:
self.api_keys_seen.append(api_key)
if self.only_accepts is not None and api_key != self.only_accepts:
raise ValueError("unknown caller")
return UserAPIKeyAuth(user_id=f"custom-auth-caller-{len(self.api_keys_seen)}")
@ -8274,6 +8277,23 @@ class TestVertexPassthroughAuthenticatesOncePerRequest:
assert custom_auth.api_keys_seen == [self.VIRTUAL_KEY]
assert not google.called
@pytest.mark.parametrize("header", ["x-goog-api-key", "x-api-key", "API-Key", "Ocp-Apim-Subscription-Key"])
def test_key_outside_litellm_headers_is_rejected_and_never_reaches_google_without_a_vertex_credential(
self, monkeypatch: pytest.MonkeyPatch, header: str
) -> None:
custom_auth: Final = _CountingCustomAuth(only_accepts=self.VIRTUAL_KEY)
with respx.mock(assert_all_called=False) as upstream:
google: Final = upstream.route().mock(return_value=httpx.Response(200))
response, recorder = self._post(
monkeypatch, f"/vertex_ai/{self.GENERATE_CONTENT}", {header: self.VIRTUAL_KEY}, custom_auth, ()
)
assert response.status_code == 401, response.text
assert self.VIRTUAL_KEY not in custom_auth.api_keys_seen
assert recorder.served_user_ids == []
assert not google.called
class TestTypeSafePassthroughRoute:
@staticmethod