diff --git a/litellm/proxy/gateway/mcp/outbound_credentials/resolver.py b/litellm/proxy/gateway/mcp/outbound_credentials/resolver.py index 658f55a13d1..29d1ec0c7cc 100644 --- a/litellm/proxy/gateway/mcp/outbound_credentials/resolver.py +++ b/litellm/proxy/gateway/mcp/outbound_credentials/resolver.py @@ -80,7 +80,9 @@ class UpstreamCredentialProvider: case SharedKey() as source: # The shared key is read straight from ServerSpec.config, not the per-user # store; it is the same credential for every caller. - return Ok(StaticHeaderAuth(config.header_for(source.value))) + return Ok( + StaticHeaderAuth(config.header_for(source.value.get_secret_value())) + ) case PerUserEnvVar(): value = self._per_user_value(subject, server) if value is None: @@ -119,7 +121,9 @@ class UpstreamCredentialProvider: return Error( CredError.of_unauthorized("passthrough: no inbound token to forward") ) - return Ok(StaticHeaderAuth(f"Bearer {subject.inbound_token}")) + return Ok( + StaticHeaderAuth(f"Bearer {subject.inbound_token.get_secret_value()}") + ) # --- arms awaiting their collaborators (typed stubs, fail closed) ---------------------- def _authorization_code( diff --git a/litellm/proxy/gateway/mcp/outbound_credentials/types.py b/litellm/proxy/gateway/mcp/outbound_credentials/types.py index f48609c7ea9..e8b44cd5fa0 100644 --- a/litellm/proxy/gateway/mcp/outbound_credentials/types.py +++ b/litellm/proxy/gateway/mcp/outbound_credentials/types.py @@ -28,7 +28,7 @@ from enum import Enum from typing import Annotated, Literal from expression import case, tag, tagged_union -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, SecretStr from typing_extensions import assert_never from ..result import Error, Ok, Result @@ -133,7 +133,7 @@ class AuthorizationCodeConfig(BaseModel): model_config = ConfigDict(frozen=True) kind: Literal[AuthSpecKind.authorization_code] = AuthSpecKind.authorization_code client_id: str - client_secret: str + client_secret: SecretStr authorization_url: str token_url: str scopes: tuple[str, ...] = () @@ -145,7 +145,7 @@ class ClientCredentialsConfig(BaseModel): model_config = ConfigDict(frozen=True) kind: Literal[AuthSpecKind.client_credentials] = AuthSpecKind.client_credentials client_id: str - client_secret: str + client_secret: SecretStr token_url: str scopes: tuple[str, ...] = () @@ -165,7 +165,7 @@ class SharedKey(BaseModel): model_config = ConfigDict(frozen=True) source: Literal["shared"] = "shared" - value: str + value: SecretStr class PerUserEnvVar(BaseModel): @@ -240,8 +240,8 @@ class AwsSigV4Config(BaseModel): region: str service: str = "bedrock-agentcore" access_key_id: str | None = None - secret_access_key: str | None = None - session_token: str | None = None + secret_access_key: SecretStr | None = None + session_token: SecretStr | None = None role_arn: str | None = None session_name: str | None = None @@ -266,7 +266,7 @@ class Subject(BaseModel): tenant_id: str subject_id: str # Opaque, already-validated inbound identity. Only `token_exchange` / `passthrough` read it. - inbound_token: str | None = None + inbound_token: SecretStr | None = None class ServerSpec(BaseModel): diff --git a/tests/mcp_tests/gateway/test_resolver.py b/tests/mcp_tests/gateway/test_resolver.py index 508e153e900..50890c8dd12 100644 --- a/tests/mcp_tests/gateway/test_resolver.py +++ b/tests/mcp_tests/gateway/test_resolver.py @@ -88,6 +88,15 @@ def test_api_key_emits_the_right_scheme(scheme: str, expected: str): assert _applied_headers(result.ok)["Authorization"] == expected +def test_secret_fields_are_masked_in_serialization(): + # SecretStr keeps the value out of model_dump / repr / logs but usable in the resolver. + config = ApiKeyConfig(key_source=SharedKey(value="SUPER-SECRET")) + dumped = config.model_dump_json() + assert "SUPER-SECRET" not in dumped + assert "**********" in dumped + assert config.key_source.value.get_secret_value() == "SUPER-SECRET" + + @pytest.mark.parametrize("source", [Byok(), PerUserEnvVar()]) def test_api_key_per_user_pulls_the_subject_credential(source: object): store = InMemoryCredentialStore(