Skip to content

fix(proxy): keep the caller's Google token on credential-less Vertex passthrough under custom auth - #38299

Merged
mateo-berri merged 6 commits into
litellm_internal_stagingfrom
litellm_fix_vertex_pt_custom_auth_google_token
Aug 26, 2026
Merged

fix(proxy): keep the caller's Google token on credential-less Vertex passthrough under custom auth#38299
mateo-berri merged 6 commits into
litellm_internal_stagingfrom
litellm_fix_vertex_pt_custom_auth_google_token

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

How it solves it:

  • Strip only the secret that actually authenticated the request
  • That is the master key, the key auth resolved, or the JWT whose claims auth resolved under JWT auth
  • sk-shaped secrets stay stripped even with no master key configured
  • Proxy-only key headers Google never reads are still dropped by name
  • No surviving Google credential still gets the clean 401

User Flow

Before: a developer whose proxy runs a custom auth plugin and has no Vertex credential sends Vertex requests with their own Google token and gets 401

  1. The proxy admin runs LiteLLM with custom_auth pointing at their own auth plugin and no Vertex credential configured on the proxy
  2. The developer mints a Google OAuth token with gcloud auth print-access-token and sends POST https://litellm-domain/vertex_ai/v1/projects/{project}/locations/global/publishers/google/models/gemini-3.5-flash-lite:generateContent with Authorization: Bearer ya29... and a contents body
  3. The custom auth plugin accepts the request
  4. The proxy answers 401 with "No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential ... send your own Google OAuth token in the Authorization header", which is exactly what they sent
  5. Nothing reaches Google and no request shows up at https://litellm-domain/ui/?page=logs
  6. Another caller who sends only a LiteLLM virtual key or the master key in Authorization also gets the 401 and that key is never forwarded to Google

After: the same request is forwarded to Google with the developer's own token and comes back 200

  1. The proxy admin runs LiteLLM with custom_auth pointing at their own auth plugin and no Vertex credential configured on the proxy
  2. The developer mints a Google OAuth token with gcloud auth print-access-token and sends POST https://litellm-domain/vertex_ai/v1/projects/{project}/locations/global/publishers/google/models/gemini-3.5-flash-lite:generateContent with Authorization: Bearer ya29... and a contents body
  3. The custom auth plugin accepts the request
  4. The proxy forwards the request to Google with that token and returns 200 with the Gemini candidates response and an x-litellm-call-id header
  5. https://litellm-domain/ui/?page=logs shows the request with real spend
  6. Another caller who sends only a LiteLLM virtual key, the master key, or (on a JWT-auth proxy) the LiteLLM JWT that authenticated still gets the 401 and that secret is still never forwarded to Google

Relevant issues

Regression introduced by #38114. First red run of the CI job: https://app.circleci.com/workflow/7e7cff9b-5f81-47bc-9377-555351c9a8f1/job/342188e8-152c-42d8-83e6-d354ea58f688

Linear ticket

Resolves LIT-6175

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Both legs share the same rig and differ only in the commit the proxies were booted from: Before at the merge base, After at the PR tip. Each leg is its own worktree, its own .venv, and its own Postgres database, with proxies booted from variants of litellm/proxy/example_config_yaml/pass_through_config.yaml, every one with --num_workers 2 --timeout_worker_healthcheck 120. Only proxy C holds a Vertex credential. Every 200 below is a real Vertex call against a real GCP project with real spend

Proxy F (JWT auth) runs on the After leg only: the JWT-stripping branch landed mid-PR, its regression proof lives in the new unit tests, and F proves the tip end-to-end. Its config sets enable_jwt_auth: true plus litellm_jwtauth.admin_allowed_routes extended with "/vertex_ai/*", validating against a local JWKS static server, and the caller authenticates with a self-minted RS256 admin JWT (scope: litellm_proxy_admin)

Proxy Auth Vertex credential on proxy Before port After port
A custom_auth_basic (accepts anything) none 27413 38386
B standard key auth, master key none 26389 31263
C custom_auth_basic service account via DEFAULT_* env 25924 33810
D example custom_auth (only sk-1234-1234) none 31136 34599
E no general_settings, no master key none 20765 37224
F JWT auth (enable_jwt_auth) none After leg only 38616

Shared shell prelude (per leg; PORT_X from the table):

TOKEN=$(GOOGLE_APPLICATION_CREDENTIALS=<service account json> gcloud auth application-default print-access-token)
VKEY=<virtual key minted on the leg's DB via /key/generate>
LJWT=<RS256 JWT signed by the key behind the local JWKS, payload {"sub": "qa-jwt-admin", "iss": "https://qa-local-idp.example.invalid", "scope": "litellm_proxy_admin", ...}>
BODY='{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}'
URL_X="http://127.0.0.1:${PORT_X}/vertex_ai/v1/projects/vertex-check-481318/locations/global/publishers/google/models/gemini-3.5-flash-lite:generateContent"

The 396-byte guard body is shown in full once per side and elided to its first clause afterwards (it is byte-identical each time), and unchanged usageMetadata / thoughtSignature fields inside Gemini 200 bodies are elided

Before (273b01a)

A1 custom auth, caller's own Google token

  1. curl -sS -i "$URL_A" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. The LiteLLM virtual key is not forwarded to Google. Configure a Vertex credential (DEFAULT_VERTEXAI_PROJECT / DEFAULT_VERTEXAI_LOCATION / DEFAULT_VERTEXAI_CREDENTIALS, or a model with use_in_pass_through: true), or send your own Google OAuth token in the Authorization header."}
    
  3. The caller's own token was stripped as if it were the virtual key: this is the regression

A2 custom auth, caller's own token, SSE stream

  1. curl -sS -i -m 60 "http://127.0.0.1:${PORT_A}/vertex_ai/v1/projects/vertex-check-481318/locations/global/publishers/google/models/gemini-3.5-flash-lite:streamGenerateContent?alt=sse" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    

A3 spend log for A1

  1. A1 returned 401 with no x-litellm-call-id header, so there is no request id to query

A4 custom auth, master key alone

  1. curl -sS -i "$URL_A" -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    

A5 custom auth, virtual key alone

  1. curl -sS -i "$URL_A" -H "Authorization: Bearer $VKEY" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    
  3. Guard body, not a Google error: the virtual key never reached Google

A6 custom auth, virtual key header plus caller token

  1. curl -sS -i "$URL_A" -H "x-litellm-api-key: $VKEY" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: 265e9867-4f98-48fd-ad47-6f0517ec9eab
    x-litellm-model-api-base: https://aiplatform.googleapis.com/v1/projects/vertex-check-481318/locations/global/publishers/google/models/gemini-3.5-flash-lite:generateContent
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hi there! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 10, "totalTokenCount": 11, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "lGiOaqCFEu6a998Pp4_eyAo"}
    
  3. curl -s "http://127.0.0.1:${PORT_A}/spend/logs?request_id=265e9867-4f98-48fd-ad47-6f0517ec9eab" -H "Authorization: Bearer sk-1234"

  4. Observed:

    [{"request_id": "265e9867-4f98-48fd-ad47-6f0517ec9eab", "model": "gemini-3.5-flash-lite", "spend": 2.53e-05, "custom_llm_provider": "vertex_ai", "call_type": "pass_through_endpoint", "api_key": "272d3ff670aa5055fe0f16c125239d37ceedc8758523bb2dd84a4db66ae5c157", "prompt_tokens": 1, "completion_tokens": 10, "status": "success"}]
    

B1 standard auth, virtual key alone

  1. curl -sS -i "$URL_B" -H "Authorization: Bearer $VKEY" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    

B2 standard auth, virtual key in x-goog-api-key

  1. curl -sS -i "$URL_B" -H "x-goog-api-key: $VKEY" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 155
    
    {"error":{"message":"Authentication Error, Malformed API Key passed in. Ensure Key has `Bearer ` prefix.","type":"auth_error","param":"None","code":"401"}}
    
  3. Auth-layer 401, not the guard; the key still never reached Google

B3 standard auth, master key alone

  1. curl -sS -i "$URL_B" -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    

B4 standard auth, virtual key header plus caller token

  1. curl -sS -i "$URL_B" -H "x-litellm-api-key: $VKEY" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: 287270b0-0fc4-48c3-9cc9-c937ac2439f8
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hi there! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 10, "totalTokenCount": 11, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "pGiOaur4DreO4_UPtpzggQM"}
    
  3. curl -s "http://127.0.0.1:${PORT_B}/spend/logs?request_id=287270b0-0fc4-48c3-9cc9-c937ac2439f8" -H "Authorization: Bearer sk-1234"

  4. Observed:

    [{"request_id": "287270b0-0fc4-48c3-9cc9-c937ac2439f8", "model": "gemini-3.5-flash-lite", "spend": 2.53e-05, "custom_llm_provider": "vertex_ai", "call_type": "pass_through_endpoint", "api_key": "28fedb09d252bdcfef4b6f94c378f3aa28ff75bc68ca303e39734257881a1495", "prompt_tokens": 1, "completion_tokens": 10, "status": "success"}]
    

B5 standard auth, Google token alone

  1. curl -sS -i "$URL_B" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 371
    
    {"error":{"message":"LiteLLM Virtual Key expected. Received=ya29****5mr7, expected to start with 'sk-'. This key has the structure of a JWT, but JWT auth is not enabled on this proxy, so it was treated as a virtual key. Set `enable_jwt_auth: true` under `general_settings` in your proxy config to authenticate with JWTs.","type":"auth_error","param":"None","code":"401"}}
    
  3. On standard auth the token is rejected by auth itself, so it never reaches the passthrough

C1 proxy credential, master key

  1. curl -sS -i "$URL_C" -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: 769b21d4-0879-4f5f-9573-875f92502e6a
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hi there! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 10, "totalTokenCount": 11, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "MGmOas7dEZ2W4_UPlqmE8AY"}
    

C2 proxy credential, caller token

  1. curl -sS -i "$URL_C" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: c7912017-10e4-41bb-97f5-117232b4432f
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hi there! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 10, "totalTokenCount": 11, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "NGmOasmzFbG74_UPqZDo6Qk"}
    

D1 strict custom auth, its key alone

  1. curl -sS -i "$URL_D" -H "Authorization: Bearer sk-1234-1234" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    

D2 strict custom auth, its key plus caller token

  1. curl -sS -i "$URL_D" -H "x-litellm-api-key: sk-1234-1234" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: 5219425c-ea1c-498e-9324-a10b4507bf0d
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hi there! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 10, "totalTokenCount": 11, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "9W-OarPCKsmfpucPmrXosQ4"}
    
  3. curl -s "http://127.0.0.1:${PORT_D}/spend/logs?request_id=5219425c-ea1c-498e-9324-a10b4507bf0d" -H "Authorization: Bearer sk-1234-1234"

  4. Observed:

    [{"request_id": "5219425c-ea1c-498e-9324-a10b4507bf0d", "model": "gemini-3.5-flash-lite", "spend": 2.53e-05, "custom_llm_provider": "vertex_ai", "call_type": "pass_through_endpoint", "api_key": "293f78d72ff582124c201b20a715dadb044d2a74d74910f160f11dfb4b336db5", "prompt_tokens": 1, "completion_tokens": 10, "status": "success"}]
    

D3 strict custom auth, fake x-goog-api-key

  1. curl -sS -i "$URL_D" -H "Authorization: Bearer sk-1234-1234" -H "x-goog-api-key: not-a-real-google-key" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    www-authenticate: Bearer realm="https://accounts.google.com/"
    x-litellm-call-id: b06e7a7d-0621-42fd-8ae7-a486b1454b6a
    
    {"error": {"code": 401, "message": "API keys are not supported by this API. Expected OAuth2 access token or other authentication credentials that assert a principal. See https://cloud.google.com/docs/authentication", "status": "UNAUTHENTICATED", "details": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "CREDENTIALS_MISSING", "domain": "googleapis.com", "metadata": {"method": "google.cloud.aiplatform.v1.PredictionService.GenerateContent", "service": "aiplatform.googleapis.com"}}]}}
    
  3. A Google error: the bogus x-goog-api-key was forwarded and Google itself rejected it

E1 no master key, caller's own Google token

  1. curl -sS -i "$URL_E" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    
  3. The same regression on a proxy with no master key at all

E2 no master key, sk-shaped value alone

  1. curl -sS -i "$URL_E" -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    

After (a3f6547)

A1 custom auth, caller's own Google token

  1. curl -sS -i "$URL_A" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: ffb0379b-501c-4262-88ca-53bfcba9466f
    x-litellm-model-api-base: https://aiplatform.googleapis.com/v1/projects/vertex-check-481318/locations/global/publishers/google/models/gemini-3.5-flash-lite:generateContent
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hello! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 9, "totalTokenCount": 10, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "0CiPapzQJsGwodAPxry0eA"}
    
  3. The caller's own token now reaches Google: the regression is fixed

A2 custom auth, caller's own token, SSE stream

  1. curl -sS -i -m 90 "http://127.0.0.1:${PORT_A}/vertex_ai/v1/projects/vertex-check-481318/locations/global/publishers/google/models/gemini-3.5-flash-lite:streamGenerateContent?alt=sse" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    content-type: text/event-stream
    x-litellm-call-id: 72e4ca31-8a66-46d2-affb-b1bca09f2c6b
    
    data: {"candidates": [{"content": {"role": "model","parts": [{"text": "Hello"}]}}], ..., "responseId": "0SiPauS6HNu5odAP5p-u2Qo"}
    
    data: {"candidates": [{"content": {"role": "model","parts": [{"text": "", ...}]},"finishReason": "STOP"}],"usageMetadata": {"promptTokenCount": 1,"candidatesTokenCount": 9,"totalTokenCount": 10, ...}, ...}
    

A3 spend log for A1

  1. curl -s "http://127.0.0.1:${PORT_A}/spend/logs?request_id=ffb0379b-501c-4262-88ca-53bfcba9466f" -H "Authorization: Bearer sk-1234" (found on first poll)

  2. Observed:

    {"request_id": "ffb0379b-501c-4262-88ca-53bfcba9466f", "model": "gemini-3.5-flash-lite", "custom_llm_provider": "vertex_ai", "spend": 2.28e-05, "call_type": "pass_through_endpoint", "api_key": "272d3ff670aa5055fe0f16c125239d37ceedc8758523bb2dd84a4db66ae5c157", "prompt_tokens": 1, "completion_tokens": 9}
    

A4 custom auth, master key alone

  1. curl -sS -i "$URL_A" -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. The LiteLLM virtual key is not forwarded to Google. Configure a Vertex credential (DEFAULT_VERTEXAI_PROJECT / DEFAULT_VERTEXAI_LOCATION / DEFAULT_VERTEXAI_CREDENTIALS, or a model with use_in_pass_through: true), or send your own Google OAuth token in the Authorization header."}
    
  3. The master key is still stripped: the fix(passthrough): stop leaking the caller's virtual key on credential-less Vertex passthrough #38114 leak protection holds

A5 custom auth, virtual key alone

  1. curl -sS -i "$URL_A" -H "Authorization: Bearer $VKEY" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    www-authenticate: Bearer realm="https://accounts.google.com/", error="invalid_token"
    x-litellm-call-id: 01ad4f5a-bae3-4332-ac14-b4f368edc100
    
    {"error": {"code": 401, "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. ...", "status": "UNAUTHENTICATED", "details": [{..., "reason": "ACCESS_TOKEN_TYPE_UNSUPPORTED", ...}]}}
    
  3. Under custom auth this key did not authenticate the request, so it is forwarded and Google rejects it: the pre-fix(passthrough): stop leaking the caller's virtual key on credential-less Vertex passthrough #38114 behavior (see Caveats)

A6 custom auth, virtual key header plus caller token

  1. curl -sS -i "$URL_A" -H "x-litellm-api-key: $VKEY" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: 62ebef12-c1f0-4162-882b-393df6e82c13
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hi there! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 10, "totalTokenCount": 11, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "0iiPaqinDZulodAPodu4wA8"}
    

B1 standard auth, virtual key alone

  1. curl -sS -i "$URL_B" -H "Authorization: Bearer $VKEY" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    
  3. Here the virtual key did authenticate, so it is stripped and never forwarded

B2 standard auth, virtual key in x-goog-api-key

  1. curl -sS -i "$URL_B" -H "x-goog-api-key: $VKEY" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 155
    
    {"error":{"message":"Authentication Error, Malformed API Key passed in. Ensure Key has `Bearer ` prefix.","type":"auth_error","param":"None","code":"401"}}
    

B3 standard auth, master key alone

  1. curl -sS -i "$URL_B" -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    

B4 standard auth, virtual key header plus caller token

  1. curl -sS -i "$URL_B" -H "x-litellm-api-key: $VKEY" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: 87703f23-8e11-41a0-b5df-9e27d2de3705
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hello! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 9, "totalTokenCount": 10, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "0iiPav_VOK2p6tkP9uu3kAo"}
    
  3. curl -s "http://127.0.0.1:${PORT_B}/spend/logs?request_id=87703f23-8e11-41a0-b5df-9e27d2de3705" -H "Authorization: Bearer sk-1234"

  4. Observed:

    {"request_id": "87703f23-8e11-41a0-b5df-9e27d2de3705", "model": "gemini-3.5-flash-lite", "custom_llm_provider": "vertex_ai", "spend": 2.28e-05, "call_type": "pass_through_endpoint", "api_key": "c7acd68871718fe80a7d1a72fa5ca5d7f4337e40d74b7973c1850ab73988063d", "prompt_tokens": 1, "completion_tokens": 9}
    

B5 standard auth, Google token alone

  1. curl -sS -i "$URL_B" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 371
    
    {"error":{"message":"LiteLLM Virtual Key expected. Received=ya29****rqOv, expected to start with 'sk-'. This key has the structure of a JWT, but JWT auth is not enabled on this proxy, so it was treated as a virtual key. Set `enable_jwt_auth: true` under `general_settings` in your proxy config to authenticate with JWTs.","type":"auth_error","param":"None","code":"401"}}
    

C1 proxy credential, master key

  1. curl -sS -i "$URL_C" -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: 0c4feedf-5ea1-47bf-a429-c6a158ad6f2c
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hi there! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 10, "totalTokenCount": 11, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "0yiPatWSL5i1odAPitKluQc"}
    
  3. Master key stripped, the proxy's own credential used

C2 proxy credential, caller token

  1. curl -sS -i "$URL_C" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: dbba7202-9eab-4d83-b855-fef32050cc37
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hello! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 9, "totalTokenCount": 10, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "1CiPavnYKPy5odAP6qq_8As"}
    

D1 strict custom auth, its key alone

  1. curl -sS -i "$URL_D" -H "Authorization: Bearer sk-1234-1234" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    
  3. The sk-shaped custom-auth key that authenticated is stripped, never forwarded

D2 strict custom auth, its key plus caller token

  1. curl -sS -i "$URL_D" -H "x-litellm-api-key: sk-1234-1234" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: 75a4eaf2-a9ea-4dcb-8b8c-6dc15b3229d2
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hello! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 9, "totalTokenCount": 10, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "1SiPauCvGr68odAPgM6oyQ4"}
    
  3. curl -s "http://127.0.0.1:${PORT_D}/spend/logs?request_id=75a4eaf2-a9ea-4dcb-8b8c-6dc15b3229d2" -H "Authorization: Bearer sk-1234-1234"

  4. Observed:

    {"request_id": "75a4eaf2-a9ea-4dcb-8b8c-6dc15b3229d2", "model": "gemini-3.5-flash-lite", "custom_llm_provider": "vertex_ai", "spend": 2.28e-05, "call_type": "pass_through_endpoint", "api_key": "293f78d72ff582124c201b20a715dadb044d2a74d74910f160f11dfb4b336db5", "prompt_tokens": 1, "completion_tokens": 9}
    

D3 strict custom auth, fake x-goog-api-key

  1. curl -sS -i "$URL_D" -H "Authorization: Bearer sk-1234-1234" -H "x-goog-api-key: not-a-real-google-key" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    www-authenticate: Bearer realm="https://accounts.google.com/"
    x-litellm-call-id: df00f80a-a870-4204-9ed4-3081bf11eb29
    
    {"error": {"code": 401, "message": "API keys are not supported by this API. Expected OAuth2 access token or other authentication credentials that assert a principal. ...", "status": "UNAUTHENTICATED", "details": [{..., "reason": "CREDENTIALS_MISSING", ...}]}}
    

E1 no master key, caller's own Google token

  1. curl -sS -i "$URL_E" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: ee5881a4-8e6a-415e-a025-715a0b5951b9
    x-litellm-model-api-base: https://aiplatform.googleapis.com/v1/projects/vertex-check-481318/locations/global/publishers/google/models/gemini-3.5-flash-lite:generateContent
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hi there! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 10, "totalTokenCount": 11, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "ACmPasCZGISr_9MP1KTWmQ4"}
    
  3. The regression is fixed on the no-master-key branch too

E2 no master key, sk-shaped value alone

  1. curl -sS -i "$URL_E" -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    
  3. sk-shaped secrets stay stripped even with no master key configured

F1 JWT auth, the authenticating LiteLLM JWT alone

  1. curl -sS -i "$URL_F" -H "Authorization: Bearer $LJWT" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 401 Unauthorized
    content-length: 396
    
    {"detail":"No Vertex AI credential is configured on this proxy and the request carried no upstream Google credential. ..."}
    
  3. JWT auth accepted the request, then the JWT that authenticated was stripped rather than forwarded to Google

F2 JWT auth, LiteLLM JWT in header plus caller token

  1. curl -sS -i "$URL_F" -H "x-litellm-api-key: $LJWT" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY"

  2. Observed:

    HTTP/1.1 200 OK
    x-litellm-call-id: 60351901-dad0-4c52-a423-2eff54c69b1c
    
    {"candidates": [{"content": {"role": "model", "parts": [{"text": "Hello! How can I help you today?", ...}]}, "finishReason": "STOP"}], "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 9, "totalTokenCount": 10, ...}, "modelVersion": "gemini-3.5-flash-lite", "responseId": "1iiPau3UCZOp4_UPipuP8Ag"}
    
  3. The JWT-authed caller's own Google token still flows through

Observations from the run, all pre-existing and left alone by this PR:

  • A5: unconsumed virtual key reached Google under custom auth
  • D3: fake x-goog-api-key forwarded; Google rejected it itself
  • Guard names DEFAULT_VERTEXAI_CREDENTIALS; router reads DEFAULT_GOOGLE_APPLICATION_CREDENTIALS
  • x-litellm-key-spend header often 0.0 despite recorded spend
  • B5 hint recommends enable_jwt_auth for a Google OAuth token

Type

🐛 Bug Fix

Caveats (if any)

Low

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

  • be0cac1 passes /live-pr-risk

  • a3f6547 passes /live-pr-risk


Note

Medium Risk
Changes auth-secret stripping on the Vertex BYO-credential path—a security-sensitive area where a mistake could leak keys upstream or break legitimate Google tokens; scope is limited to credential-less passthrough with expanded unit coverage.

Overview
Fixes a #38114 regression where credential-less Vertex passthrough dropped Authorization (or other credential headers) using header precedence, so valid bring-your-own Google tokens were treated as LiteLLM keys and requests 401’d under custom auth or JWT auth.

Credential-less forwarding now removes headers by what actually authenticated (user_api_key_dict), not by which header won precedence: master key (hmac), JWT matching stored jwt_claims, or virtual keys compared via the same hash auth uses for api_key. Proxy-only key headers are still dropped by name; sk-* values stay stripped when no master key is configured.

_prepare_vertex_auth_headers takes user_api_key_dict and passes it into _forwarded_headers_for_credentialless_vertex_passthrough. Tests add JWT, master-key, and custom-auth regression cases; a guardrails test drops a fake proxy_server module shim.

Reviewed by Cursor Bugbot for commit a3f6547. Bugbot is set up for automated code reviews on this repo. Configure here.

…ertex passthrough

PR #38114 dropped whichever header user_api_key_auth would read the caller's
key from, by precedence. Under custom_auth, JWT auth, or no master key that
header is the caller's own Google token, so the bring-your-own-credentials
Vertex branch answered 401 to every valid request.

A header value is now dropped only when it is the master key or when its
hash is the api_key that authenticated the request, so a Google token that
auth never consumed keeps flowing while a LiteLLM key still never reaches
Google.

test_passthrough_post_call_guardrails.py no longer plants a MagicMock
proxy_server module in sys.modules at import, which poisoned sibling tests
that read module globals at call time.
@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR updates credential-less Vertex passthrough to preserve caller-provided Google credentials while removing the proxy secret that authenticated the request.

  • Uses resolved proxy authentication state, including JWT claims, to classify secrets.
  • Retains the existing local rejection when no upstream Google credential survives filtering.
  • Adds regression coverage for custom authentication, JWT authentication, master keys, and credential-less configurations.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py Refines credential classification and forwarding for credential-less Vertex passthrough; no eligible follow-up defect remains.
tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py Adds focused regression tests for preserving Google credentials and stripping authenticated proxy secrets.
tests/test_litellm/proxy/pass_through_endpoints/test_passthrough_post_call_guardrails.py Removes an obsolete proxy-server import shim without weakening the guardrail assertions.
tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py Updates direct route tests to supply the newly required authentication context.

Reviews (5): Last reviewed commit: "fix(proxy): strip the JWT that authentic..." | Re-trigger Greptile

Comment thread litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py Outdated
Comment thread litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py Outdated
authenticated_key: Final = user_api_key_dict.api_key
if authenticated_key is None:
return False
return hmac.compare_digest(hash_token(normalized).encode(), authenticated_key.encode())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Custom-auth credentials can be forwarded upstream

UserAPIKeyAuth only converts sk- credentials to a plain SHA-256 value; opaque credentials remain unchanged and JWT credentials use a hashed-jwt- prefix. Consequently, this hash-only comparison returns false when a custom auth handler returns UserAPIKeyAuth(api_key=api_key) for either form, and the same credential in Authorization is forwarded to Google. Track whether the auth path actually consumed the header as explicit server-side provenance rather than inferring it from the stored key representation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under custom auth the Authorization value is the caller's own Google token, so marking it consumed strips it and brings back the 401 fixed here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detail. I can't automatically confirm this is safe to dismiss, so I'm leaving the thread open for a maintainer to make the call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validator applies the same transform on both sides, so opaque and JWT custom-auth credentials strip; tests custom-auth-echoing-opaque-credential and custom-auth-echoing-jwt prove it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detail. I can't automatically confirm this is safe to dismiss, so I'm leaving the thread open for a maintainer to make the call.

@veria-ai

veria-ai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

PR overview

This pull request updates credential-less Vertex passthrough behavior under custom authentication so the caller’s Google token is preserved when forwarding requests.

One security issue remains open: under certain custom-auth configurations, an opaque or JWT authorization credential consumed by the proxy can also be forwarded to Google. This could disclose caller credentials to the upstream service, though exploitation depends on the custom authentication path and credential format. No issues have yet been addressed.

Open issues (1)

Fixed/addressed: 0 · PR risk: 5/10

@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_fix_vertex_pt_custom_auth_google_token (a3f6547) with litellm_internal_staging (cdb60af)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (95285c3) during the generation of this report, so cdb60af was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: JWT auth tokens leak upstream
    • Extended _is_authenticated_caller_secret to also treat a JWT-shaped header value as the authenticating secret when user_api_key_dict.jwt_claims is set, matching production JWT auth where api_key is None or holds a JWT-mapped virtual key hash, so the caller's JWT is stripped from Authorization before the surviving-credential guard runs.

Create PR

Or push these changes by commenting:

@cursor push a2cdaa531e
Preview (a2cdaa531e)
diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py
--- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py
+++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py
@@ -1817,11 +1817,14 @@
 
 def _is_authenticated_caller_secret(value: str, user_api_key_dict: UserAPIKeyAuth) -> bool:
     """Whether a header value is the master key or the key ``user_api_key_auth`` stored as ``api_key``."""
+    from litellm.proxy.auth.handle_jwt import JWTHandler
     from litellm.proxy.proxy_server import master_key
 
     normalized: Final = _normalize_credential_value(value)
     if master_key is not None and hmac.compare_digest(normalized.encode(), master_key.encode()):
         return True
+    if user_api_key_dict.jwt_claims is not None and JWTHandler.is_jwt(token=normalized):
+        return True
     authenticated_key: Final = user_api_key_dict.api_key
     if authenticated_key is None:
         return False

diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py
--- a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py
+++ b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py
@@ -3823,6 +3823,16 @@
         [
             pytest.param("modified_key", UserAPIKeyAuth(api_key="modified_key"), id="custom-auth-echoing-opaque-credential"),
             pytest.param(LITELLM_JWT, UserAPIKeyAuth(api_key=LITELLM_JWT, user_id="jwt-subject"), id="jwt-auth-consuming-header"),
+            pytest.param(
+                LITELLM_JWT,
+                UserAPIKeyAuth(api_key=None, user_id="jwt-subject", jwt_claims={"sub": "jwt-subject"}),
+                id="jwt-only-auth-with-null-api-key",
+            ),
+            pytest.param(
+                LITELLM_JWT,
+                UserAPIKeyAuth(api_key=VKEY, user_id="jwt-subject", jwt_claims={"sub": "jwt-subject"}),
+                id="jwt-mapped-virtual-key-with-jwt-still-in-authorization",
+            ),
         ],
     )
     async def test_non_sk_litellm_credential_that_authenticated_is_rejected_not_forwarded(

You can send follow-ups to the cloud agent here.

Comment thread litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a3f6547. Configure here.

@mateo-berri
mateo-berri enabled auto-merge August 26, 2026 18:21
@mateo-berri
mateo-berri merged commit def5ca6 into litellm_internal_staging Aug 26, 2026
84 checks passed
@mateo-berri
mateo-berri deleted the litellm_fix_vertex_pt_custom_auth_google_token branch August 26, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants