{"record":{"id":"a1f4e4587da29938","repo":"BerriAI/litellm","slug":"challenge-body-if-challenge-body-is-not-none-else","errorCode":null,"errorMessage":"challenge.body if challenge.body is not None else error.summary","messagePattern":"challenge\\.body if challenge\\.body is not None else error\\.summary","errorType":"http","errorClass":"HTTPException","httpStatus":401,"severity":"error","filePath":"litellm/proxy/_experimental/mcp_server/outbound_credentials/adapter.py","lineNumber":273,"sourceCode":"        return ClientSecretAuth(client_secret=SecretStr(server.client_secret))\n    return None\n\n\ndef _id_jag_subject_token_type(server: MCPServer) -> str:\n    \"\"\"ID-JAG asserts the user's id_token, so the token-exchange access_token default maps to id_token;\n    an explicitly configured value (e.g. a SAML2 assertion type) is honored verbatim.\"\"\"\n    configured: Final = server.subject_token_type\n    if configured and configured != _TOKEN_EXCHANGE_SUBJECT_TOKEN_DEFAULT:\n        return configured\n    return _ID_JAG_SUBJECT_TOKEN_DEFAULT\n\n\ndef raise_public(error: CredError) -> NoReturn:\n    \"\"\"Map a resolver CredError onto the proxy's public HTTP contract. The one edge that raises.\"\"\"\n    match error.tag:\n        case \"unauthorized\":\n            challenge: Final = error.unauthorized\n            raise HTTPException(\n                status_code=401,\n                detail=challenge.body if challenge.body is not None else error.summary,\n                headers=({\"WWW-Authenticate\": challenge.www_authenticate} if challenge.www_authenticate else None),\n            )\n        case \"misconfigured\":\n            raise HTTPException(status_code=500, detail=error.summary)\n        case \"upstream_unavailable\":\n            raise HTTPException(status_code=503, detail=error.summary)\n        case \"unsupported_mode\":\n            raise HTTPException(status_code=500, detail=error.summary)\n        case \"precondition_required\":\n            raise HTTPException(status_code=412, detail=error.summary)\n        case \"not_implemented\":\n            raise HTTPException(status_code=501, detail=error.summary)\n    assert_never(error.tag)\n\n\ndef oauth_protected_resource_path(root_path: str, server: MCPServer) -> str:","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/_experimental/mcp_server/outbound_credentials/adapter.py#L255-L291","documentation":"raise_public maps an outbound-credential resolution failure tagged unauthorized onto the proxy's public contract: HTTP 401 whose detail is the challenge body (or the CredError summary) and whose WWW-Authenticate header relays the upstream challenge. It fires when the proxy must authenticate to an upstream MCP server (per-user OAuth, delegated auth, or token exchange) and the stored credential was rejected by the upstream IdP or server.","triggerScenarios":"Calling an MCP tool or the tools/list REST facade for a server with delegated/per-user upstream OAuth where the user's or key's tokens are expired, revoked, or invalid; the token endpoint or upstream returned 401/invalid_grant during resolve_credentials; the challenge is relayed verbatim so standards-compliant MCP clients can start the upstream OAuth flow.","commonSituations":"Per-user OAuth tokens gone stale (IdP rotation window passed, user inactive); user or admin revoked consent for the OAuth app; refresh-token rotation invalidated the stored token; upstream API keys rotated without updating the stored credential.","solutions":["Re-authenticate: follow the WWW-Authenticate challenge on the 401 (it points at the server's RFC 9728 protected-resource metadata) and run the OAuth flow to mint fresh credentials.","If re-auth keeps failing, delete the stored credential (per-user/BYOK OAuth UI or DB) and re-consent from scratch.","For token-exchange servers, verify the gateway's client credentials and audience/scope - an IdP rejecting the gateway itself is a different code path (500), but a mis-scoped user token shows up here too."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"resp = await client.post(f\"{proxy}/mcp/tool-call\", json=payload, headers=headers)\nif resp.status_code == 401:\n    challenge = resp.headers.get(\"www-authenticate\", \"\")\n    if \"resource_metadata=\" in challenge:\n        await reauthorize_upstream(challenge)  # run the advertised OAuth flow, store fresh creds\n        resp = await client.post(f\"{proxy}/mcp/tool-call\", json=payload, headers=headers)\nresp.raise_for_status()","preventionTips":["Treat 401 + WWW-Authenticate from the proxy as a re-auth signal, not a hard failure.","Refresh per-user OAuth credentials proactively on a schedule shorter than the IdP token lifetime.","Alert when re-auth loops repeat - usually revoked consent or a rotated app secret."],"tags":["oauth","http-401","upstream-auth","credentials","www-authenticate","mcp"],"backgroundTag":"upstream-oauth-token-rejected","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}