BerriAI/litellm · error · HTTPException
MCPJWTSigner: incoming token verification failed: {exc}
Error message
MCPJWTSigner: incoming token verification failed: {exc} What it means
MCPJWTSigner wraps the entire incoming-token verification step in a try/except; any exception (PyJWT signature/expiry/audience errors, JWKS fetch failures, the missing-jwks_uri ValueError, introspection errors) is logged and re-raised as HTTPException 401 with the original exception text embedded. The inner message is the real diagnosis; this 401 is the uniform 'unauthenticated' surface.
Source
Thrown at litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py:820
# Three-dot pattern → JWT; otherwise opaque.
is_jwt: Final = raw_token.count(".") == 2
try:
if is_jwt:
jwt_claims = await self._verify_incoming_jwt(raw_token)
elif self.token_introspection_endpoint:
jwt_claims = await self._introspect_opaque_token(raw_token)
else:
verbose_proxy_logger.warning(
"MCPJWTSigner: access_token_discovery_uri is set but the "
"incoming token appears to be opaque and no "
"token_introspection_endpoint is configured. "
"Proceeding without incoming token verification."
)
except Exception as exc:
verbose_proxy_logger.error("MCPJWTSigner: incoming token verification failed: %s", exc)
from fastapi import HTTPException
raise HTTPException(
status_code=401,
detail={"error": (f"MCPJWTSigner: incoming token verification failed: {exc}")},
)
elif not raw_token and self.access_token_discovery_uri:
verbose_proxy_logger.debug(
"MCPJWTSigner: access_token_discovery_uri configured but no Bearer "
"token found in request (API-key auth request — skipping verification)."
)
# Fall back to LiteLLM-decoded JWT claims (available when proxy uses JWT auth).
if jwt_claims is None:
jwt_claims = user_api_key_dict.jwt_claims
# ------------------------------------------------------------------
# FR-15: Validate required claims
# ------------------------------------------------------------------
self._validate_required_claims(jwt_claims)
View on GitHub (pinned to 77b7c6c40c)
Solutions
- Read the {exc} text in the 401 detail - it names the actual failure (expired, audience, jwks_uri, network) and dictates the fix
- For exp/iat failures, sync the proxy host clock (NTP) or account for clock skew
- For issuer/audience failures, align verify_issuer/verify_audience with the claims the IdP actually mints
- For fetch failures, ensure the proxy can reach the discovery and JWKS URLs (no firewall, valid TLS, correct DNS)
Example fix
# before - verification config does not match the token's claims litellm_params: access_token_discovery_uri: https://idp.example.com/.well-known/openid-configuration verify_issuer: https://wrong-issuer.example.com verify_audience: my-mcp-api # after - matches the IdP's issued claims (check with jwt.io) litellm_params: access_token_discovery_uri: https://idp.example.com/.well-known/openid-configuration verify_issuer: https://idp.example.com verify_audience: mcp-gateway
Defensive patterns
Strategy: try-catch
Validate before calling
import httpx, jwt as pyjwt
def verify_token_will_pass(token: str, discovery_uri: str, issuer: str, audience: str) -> None:
doc = httpx.get(discovery_uri, timeout=10).json()
assert doc.get("jwks_uri"), "discovery doc lacks jwks_uri"
jwks = httpx.get(doc["jwks_uri"], timeout=10).json()
header = pyjwt.get_unverified_header(token)
assert any(k.get("kid") == header.get("kid") for k in jwks.get("keys", [])), "signing kid not in JWKS"
claims = pyjwt.decode(token, options={"verify_signature": False})
assert issuer in (claims.get("iss"), None) or claims.get("iss") == issuer, "issuer mismatch"
assert audience in claims.get("aud", []), "audience mismatch" Try / catch
import openai
try:
resp = client.responses.create(model=deployment, tools=mcp_tools, input=prompt)
except openai.AuthenticationError as e:
msg = getattr(e, "body", {}).get("error", "") if isinstance(getattr(e, "body", None), dict) else str(e)
if msg.startswith("MCPJWTSigner: incoming token verification failed"):
reason = msg.rsplit(":", 1)[-1].strip()
if "expired" in reason:
token = refresh_access_token()
return retry_with(token)
log_and_surface_auth_diagnostic(reason)
raise Prevention
- Keep proxy host clocks NTP-synced to avoid spurious exp/iat failures
- Match verify_issuer/verify_audience against decoded tokens, not against documentation
- Ensure egress from the proxy to the discovery and JWKS URLs is allowed (firewall, TLS, DNS)
- Refresh tokens before expiry in clients so verification never sees an expired token
When it happens
Trigger: An MCP request with a Bearer token that fails verification: expired token (exp), wrong issuer/audience vs verify_issuer/verify_audience, signature not matching JWKS keys, unknown kid, clock skew on iat/nbf, unreachable discovery/JWKS endpoint, or the opaque-token path hitting misconfigured introspection.
Common situations: Tokens from a different IdP than the one configured; system clocks drifted so valid tokens read expired; JWKS endpoint blocked by firewall; rotated signing keys with stale cache; test tokens signed with a dev key against a prod discovery URI.
Related errors
- byok_auth_required
- MCPJWTSigner guardrail requires a guardrail_name
- MCPJWTSigner guardrail '{guardrail_name}' has mode='{mode}'
- MCPJWTSigner: ttl_seconds must be > 0, got {resolved_ttl}
- MCPJWTSigner: token_introspection_endpoint is required for o
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/826a8fbe1f023ac7.
Report an issue: GitHub.