BerriAI/litellm · error · SubjectTokenRejected
IdP rejected the subject token (HTTP {status_code})
Error message
IdP rejected the subject token (HTTP {status_code}) What it means
During RFC 8693 token exchange for an MCP server, the IdP's token endpoint returned a 4xx whose OAuth error code indicts the caller's subject token (e.g. invalid_grant from an expired/revoked refresh token or withdrawn consent) rather than the gateway's own credentials (invalid_client, unauthorized_client, unsupported_grant_type, invalid_target, invalid_scope map to a 500 instead). The proxy wraps it as SubjectTokenRejected and surfaces a 401 to the caller, preserving any IdP step-up claims blob.
Source
Thrown at litellm/proxy/_experimental/mcp_server/outbound_credentials/token_exchange_provider.py:92
headers: Final = {"Accept": "application/json", **client_auth_headers}
try:
client: Final = get_async_httpx_client(llm_provider=httpxSpecialProvider.MCP) # pyright: ignore
response: Final = await client.post(url, headers=headers, data=form) # pyright: ignore
response.raise_for_status() # pyright: ignore
parsed: Final[object] = response.json() # pyright: ignore
except httpx.HTTPStatusError as status_err:
status_code: Final = status_err.response.status_code
if 400 <= status_code < 500:
oauth_error, claims = _oauth_error_fields(status_err.response)
if oauth_error in _GATEWAY_FAULT_OAUTH_ERRORS:
verbose_logger.warning(
"MCP token exchange rejected as %s (HTTP %d); check the gateway client credentials, "
"audience, and scope for this server",
oauth_error,
status_code,
)
raise TokenExchangeClientError(oauth_error) from status_err
raise SubjectTokenRejected(
f"IdP rejected the subject token (HTTP {status_code})",
claims=claims,
) from status_err
verbose_logger.warning("MCP token exchange request failed: %s", status_err)
return None
except Exception as exc: # noqa: BLE001
verbose_logger.warning("MCP token exchange request failed: %s", exc)
return None
if not isinstance(parsed, dict):
# A valid-but-non-object JSON body (list/string/number) would crash the field parsing; map it
# to a miss so it surfaces as a typed upstream_unavailable, not a 500.
verbose_logger.warning("MCP token exchange returned non-object JSON (%s)", type(parsed).__name__)
return None
return parsed # pyright: ignore
def build_token_exchanger() -> OboTokenExchanger:
return OboTokenExchanger(View on GitHub (pinned to 77b7c6c40c)
Solutions
- Re-authenticate the user against the upstream IdP (re-run the OAuth flow) to obtain a fresh subject token, then retry the tool call.
- If the 401 payload carries a claims blob, replay the step-up challenge to the IdP (MSAL-style claims challenge) to satisfy Conditional Access.
- Check IdP sign-in/audit logs for the exact rejection reason (revocation, CA policy, audience) if re-auth does not clear it.
Defensive patterns
Strategy: try-catch
Try / catch
resp = await client.post(f"{proxy}/mcp/tool-call", json=payload, headers=headers)
if resp.status_code == 401 and "IdP rejected the subject token" in resp.text:
claims = extract_claims(resp) # step-up blob if the IdP sent one
await reauthenticate_user(claims=claims) # re-run upstream OAuth (MSAL handles claims challenge)
resp = await client.post(f"{proxy}/mcp/tool-call", json=payload, headers=headers)
resp.raise_for_status() Prevention
- Refresh delegated subject tokens on a schedule well inside the IdP's token lifetime.
- Implement the claims-challenge (step-up) path in the client so Conditional Access prompts re-auth cleanly.
- Monitor for repeated 401 token-exchange rejections per user - usually revoked consent needing manual re-auth.
When it happens
Trigger: Per-user/delegated token exchange where the user's upstream token expired, was revoked, or the IdP demands step-up: POST to the token endpoint answers 400/401 with invalid_grant (or similar non-gateway code) and this exception replaces the credential with a typed rejection.
Common situations: Entra ID Conditional Access/CAE requiring re-authentication; refresh tokens past the rotation window for inactive users; tenant admin revoked the OAuth application's consent; subject token audience changed after an IdP migration.
Related errors
- challenge.body if challenge.body is not None else error.summ
- Token exchange failed: {exc}
- Token exchange response missing fields: {data}
- invalid_request
- error.summary
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/6aa6e07edb86495c.
Report an issue: GitHub.